5 분 소요

Introduction

리액트 (React) 라이브러리하고 리덕스 (Redux)만 사용했던 기존의 어떤 프로젝트를 Next.jsRedux-Saga를 사용하여 새롭게 리뉴얼을 하고 있다. 기존 프로젝트때 했던 대로 리덕스 세팅을 하니 여러가지 에러가 발생했고, 그 에러들을 조사하다가 Next.js 에서는 조금 다른 방식으로 리덕스 초기 세팅이 이루어진다는걸 알게 되었고, 공부하면서 알게된 것들을 기록해보고자 한다.

Redux

Redux를 간단하게 설명하자면, JavaScript 애플리케이션에서 상태 관리를 위한 라이브러리 라고 할 수 있겠다. Redux는 크게 Action, Reducer, Store로 역할이 나뉘어 지고 다음과 같이 정의된다.

  • Action: 상태를 변경하기 위한 신호
  • Reducer: 액션에 따라 상태를 업데이트 하는 함수
  • Store: 상태가 저장되는 장소

Redux는 이 세가지 개념을 통해 애플리케이션의 모든 상태를 전역적으로 관리한다. 이를 통해 상태 관리의 복잡성을 줄이고 디버깅 및 테스트를 용이하게 한다.

Redux with React

Redux는 주로 React와 함께 사용된다. Redux를 사용한다는 것은 컴포넌트 사용에 필요한 상태 (state) 들을 중앙 저장소 (store)에 저장을 하고 관리를 한다는 뜻이다. 클라이언트의 요청을 받은 백엔드는 그 요청을 처리해서 응답을 보내준다. 그 응답은 종류 (성공 또는 실패)에 따라 Reducer에서 상태 값들이 업데이트 되고 중앙 저장소에 저장 된다. 컴포넌트는 이 저장소에 저장된 상태들을 서로 공유한다. 더 자세한 설명은 Next.js에서 사용하는 Redux를 설명할 때 하도록 하겠다.

이 개념을 이렇게 먼저 설명하는 이유는 다음 챕터에서 설명하게 될 Next.jsRedux는 조금 다르기 때문이다.

Next.js

Rendering Environments in Next.js를 보면 Next.js 에는 두가지 렌더링 환경이 있는걸 알 수 있다. 하나는 Server 그리고 다른 하나는 Client. 그리고 Server 에서 렌더링 되는 컴포넌트를 Server Component, 서버 컴포넌트 라고 부르고 Client 에서 렌더링 되는 컴포넌트를 Client Component, 클라이언트 컴포넌트 라고 부른다. Next.js 버전 13 이상 부터는 프레임워크에서 생성되는 모든 컴포넌트를 서버 컴포넌트 로 정의한다.

그리고 Next.js 13 로 넘어오면서 Routing 아키텍쳐에 새로운 방식이 도입되었다. 자세한 내용은 Routing in Next.js 에서 볼 수 있다. 간략하게 말하자면 Next.js 13 까지는 Page Router, 즉 넥스트는 /pages 폴더를 감지하고 그 안에 있는 파일들이 곧 URL 경로가 되는 것이다. Next.js 의 새로운 라우팅 방식은 /app 디렉토리 안에 폴더와 파일을 생성해 경로를 정의하고, 각 경로는 page.tsx파일로 설정되는 방식이다. 이 라우팅 시스템은 기존의 Page Router 방식을 좀 더 유연하게 하며, 특히 React Server Components (RSCs) 를 지원한다.

Redux with Next.js

이 새로운 아키텍쳐는 Redux 세팅을 할 때 세가지를 권장하고 있다 (참조: Redux Toolkit Setup with Next.js).

  1. 글로벌 스토어 금지: Redux 스토어가 글로벌 변수로 정의되면 요청 간에 공유가 될 수 있다. 이는 데이터 오염을 초래할 수 있기 때문에 요청별로 스토어가 새로 생성되어야 한다.
  2. RSC (React Server Component) 에서 Redux 사용 금지: React Server Component, RSC, 는 상태를 관리하지 않고 훅 (hook)이나 컨텍스트를 사용할 수 없다. 따라서 RSC에서 Redux 스토어에 접근하거나 수정하는 것은 Next.js 와 맞지 않다.
  3. 스토어에 변할 수 있는 데이터만 저장: Redux는 글로벌 및 변할 수 있는 데이터를 관리하기 위해 사용해야 한다. 이는 서버와 클라이언트 간의 데이터 관리와 일관성을 유지하는데 도움이 된다.

여기서 내가 중요하게 생각했던 부분은 1번이었다. 리액트를 사용할 때는 스토어는 딱 한 개 생성이 되고 그 스토어에 저장되는 상태들을 컴포넌트들이 공유를 했다. 하지만 이 글로벌 스토어가 금지라니, 처음엔 이해가 가질 않았다.

데이터 오염

위의 1번 내용을 보면, 리덕스 스토어가 글로벌 변수로 정의되면 데이터 오염을 초래할 수 있다고 나와있다.
이 데이터 오염에 대해서 아주 짧은 시나리오를 소개하겠다.

예: 쇼핑몰 웹사이트. 상황: 쇼핑몰 웹사이트에서 Redux를 사용하여 쇼핑 카트 상태를 관리한다고 가정. 이 웹사이트는 서버사이드 렌더링 을 사용하여 페이지를 렌더링함.

시나리오:

  1. 사용자 A 가 웹사이트에 접속, 특정 상품을 쇼핑 카트에 추가. 이 요청은 서버에서 처리가 되고, 리덕스 스토어에 저장. 예를 들어, Redux 스토어는 cart: [{id: 1, name: Product A, quantity: 1}]의 상태를 가지게 됨.

  2. 동일한 서버 인스턴스에서 사용자 B가 웹사이트에 접속하여 다른 상품을 쇼핑카트에 추가. 만약 Redux 스토어가 글로벌 변수로 정의되어 있다면, 사용자 B의 쇼핑 카트 상태가 사용자 A의 상태를 덮어쓰거나, 기존 상태에 추가될 수 있음. 예를 들어 사용자 B가 Product B를 추가하면 Redux 스토어는 다음과 같이 될 수 있다.
     cart:[{id: 1, name: Product A, quantity: 1},
     {id: 2, name: Product B, quantity: 1}]
    
  3. 이제 사용자 A가 페이지를 새로고침하면 자신의 쇼핑 카트에 Product B가 추가된 것을 보게 될 수 있고 이 현상을 “데이터가 오염되었다” 라고 표현할 수 있다.

이 시나리오를 봐도 사실 잘 이해가 가질 않았다.

왜 서로 다른 사용자가 요청을 날리는데 이 요청에 대한 결과들이 서로 섞일 수 있는거지?

이 질문에 대한 답을 하기 위해선 다음과 같은 정보를 알아야 한다.

  • Next.js에서는 클라이언트 (브라우저)의 요청을 프론트 서버에서 처리를 한다.
  • React 방식인 클라이언트 사이드 렌더링은 클라이언트의 요청을 클라이언트에서 처리를 한다.

클라이언트 사이드 렌더링 (CSR)

위에서 말했듯이 클라이언트 사이드 렌더링에선 모든 요청 처리는 클라이언트 사이드에서 일어난다. 클라이언트 사이드에서 일어나는 요청 처리는 다음과 같이 정리가 될 수 있다.

  1. 초기 요청: 사용자의 초기 요청은 최초로 프론트엔드 서버로 전송되고 브라우저는 프론트엔드 서버로부터 HTMLJavaScript를 포함한 정적 파일들을 다운로드 한다. 이 파일에는 React 컴포넌트와 Redux 초기 설정이 포함된다. 그러나 데이터는 포함되지 않고, 데이터를 가져오기 위한 로직만 포함이 된다.
  2. 데이터 페칭: 데이터를 가져오기 위한 요청은 클라이언트 (브라우저) 에서 발생하며, 브라우저는 백엔드 서버에 비동기적으로 API 요청을 보낸다.
  3. Redux 스토어: 브라우저에서 전역 Redux 스토어가 생성되며, 이 스토어는 브라우저 메모리에서 관리된다.
  4. 데이터 오염: CSR에서는 각 브라우저 인스턴스가 독립적인 환경을 유지하므로, 다른 사용자의 상태나 데이터가 공유되는 일은 없다.

아래의 이미지는 이것을 잘 설명한다.
CSR

서버 사이드 렌더링 (SSR)

Next.js는 서버 사이드 렌더링 (SSR)을 지원하며, 이는 첫 번째 요청 시 서버에서 HTML을 생성하여 클라이언트로 보내는 것을 의미한다. “서버” 에서 요청이 처리 된다는 뜻은 “동일한 서버” 인스턴스에서 처리가 된다는 뜻이고, 여러 클라이언트 요청이 하나의 서버에서 처리가 된다는 뜻이다. 따라서 상태관리를 전역적으로 관리하면 위에서 말한 데이터 오염이 발생할 수 있다. 이를 방지하기 위해서 각 요청마다 독립적인 Redux 스토어를 생성해야 한다.

하지만 한가지 짚고 넘어가야 할 점이 있다. 요청별로 생성되는 Redux스토어는 기본적으로 서버에서 생성되고 클라이언트로 전달 된다. 여기서 중요한 점은 서버에서 스토어를 생성한 후 클라이언트로 HTML과 함께 초기 상태를 전달하고, 클라이언트에서는 이 상태를 사용해 스토어를 재구성한다는 것이다.

스토어의 관리 과정

  1. 서버 측:
    • 각 클라이언트 요청이 들어올 때마다 서버에서 새로운 Redux 스토어가 생성된다.
    • 서버는 클라이언트에 응답할 HTML을 생성할 때, 이 스토어의 초기 상태를 클라이언트에 포함시킨다.
  2. 클라이언트 측:
    • 클라이언트는 서버로부터 받은 HTML과 초기 상태 데이터를 사용해 Redux 스토어를 재구성한다.
    • 이후의 상태 관리는 클라이언트 측에서 이루어진다.

리덕스의 스토어는 서버에서 생성되지만 스토어의 초기 상태는 클라이언트로 전송하고 클라이언트는 이것을 바탕으로 리덕스 스토어를 재 구성하게 된다. 따라서 하나의 클라이언트에서 발생하는 추가적인 요청들은 기존의 클라이언트 측 Redux 스토어에서 상태를 관리하며 새로운 요청마다 스토어가 새로 생성되지는 않는다.

아래의 이미지는 이것을 잘 설명한다.
SSR

Conclusion

오늘 공부한 내용은 간단하게 다음과 같이 정리할 수 있다.

  1. 리액트는 클라이언트 사이드 렌더링 방식으로 클라이언트의 요청을 처리한다. 이때 Redux 스토어와 같은 상태 관리 저장소는 전역적으로 한 개만 생성되며 브라우저의 메모리에서 관리된다. 클라이언트 사이드 렌더링이란 클라이언트는 초기 요청에 프론트엔드 서버로 부터 HTML, JavaScript와 같은 정적 파일들을 다운로드 받고 이후 데이터 요청은 브라우저에서 바로 백엔드 서버로 요청을 보낸다.

  2. Next.js는 버전 13 부터 Routing 에 새로운 방식이 도입되었는데, 바로 AppRouter 방식이다. 이 방식을 Redux와 함께 사용할 때 Next.js에서 세가지 사항을 권장한다. 이 문서는 그 중 첫 번째 조건인 “글로벌 스토어의 사용 금지”에 대해 다루고 있다.
    리덕스 스토어는 서버사이드 렌더링에선 하나의 서버 인스턴스에서 생성이 되며, 이 스토어가 글로벌 스토어로 될 경우에 여러 클라이언트의 요청 (결과들)이 하나의 스토어에 저장이 되고 이는 데이터 오염을 초래 할 수 있다. 이를 방지하기 위해 리덕스 스토어는 클라이언트 요청별로 새로 생성이 된다.
    여기서 핵심은 리덕스 스토어는 서버에서 생성이 된 후 클라이언트로 초기 상태가 전송이 된다는 점이다. 이후 클라이언트는 리덕스 스토어를 재구성해서 상태를 관리하기 때문에 하나의 클라이언트에서 발생하는 추가적인 요청마다 스토어가 새로 생성되지는 않는다.

댓글남기기