DEV Community

Ray Kim
Ray Kim

Posted on • Updated on

React SSR 아키텍처 - Redux Integration

리액트에서 공식적으로 지원하는 ReactDOM.hydrateReactDOMServer.renderToString을 통해 성공적으로 SSR된 리액트앱을 사용자에게 전달할 수 있었다. 하지만 이 방식으론 동적페이지가 아닌 상태가 존재하지 않는 간단한 페이지 밖에 렌더링하지 못한다.

상태관리 라이브러리인 Redux를 단순히 리액트 앱에 주입하면 될 것 같지만 SSR에선 store도 결국 서버에서 만들어야 한다.


preloadedState

서버에서 아무런 대응없이 상태를 주입한다면 클라이언트에서 새로운 요청을 할 때마다 새로운 상태를 만들 수밖에 없다.

이 말은 즉, 클라이언트에서 Redux 상태를 지지고 볶아도 새로운 요청을 보내면 페이지 상태가 초기화된다는 말이다.

// server code
function renderer(/* Express Request */ req) {
  // 매 요청마다 새로운 `store`이 만들어진다
  const store = createStore(/* reducers, preloadedState, enhancers */);

  const content = renderToString(
    <Provider store={store}>
      <App />
    </Provider>
  );

  return `
    <html>
      <body>
        <div id="app">${content}</div>
        <script src="bundle.js"></script>
      </body>
    </html>
  `;
}
Enter fullscreen mode Exit fullscreen mode

문제의 해결 방법은 꽤나 직관적이다 - 서버에서 초기상태(preloadedState)를 관리하여 store을 만들어주면 된다.

이렇게 하면 store을 기반으로 리액트앱이 빌드되고, 위처럼 content를 통해 HTML에 주입된다.

하지만 역시나 문제가 생긴다. 바로 이 preloadedState가 클라이언트에게 없다는 것이다. preloadedState를 이용해 리액트앱을 서버에서 빌드하여 클라이언트에게 보내는 것까진 괜찮지만, 정작 클라이언트는 '상태'는 받고있지 않다.

클라이언트에 preloadedState가 없다면 서버와 클라이언트의 상태가 다르다는 것을 뜻하며, 상태가 다르니 만들어지는 리액트앱도 다르다. 즉, hydration 과정에 문제가 생긴다.

Redux 공식문서에서는 이 문제를 해결하기 위해 preloadedStateJSON.stringify화 시켜 window 오브젝트에 주입하는 방법을 알려주고 있다.

return `
  <html>
    <body>
      <div id="app">${content}</div>
      <script>
        window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(
          /</g,
          '\\u003c'
        )}
      </script>
      <script src="bundle.js"></script>
    </body>
  </html>
`;
Enter fullscreen mode Exit fullscreen mode

replace(/</g>, '\\u003c')는 serialization을 위한 것

위와 같이 preloadedState(window.__PRELOADED_STATE__)를 HTML에 주입시킬 경우 클라이언트에서도 이를 이용한 store을 만들고 관리할 수 있다.

const store = createStore(
  /* reducers */,
  window.__PRELOADED_STATE__, // HTML에 주입된 preloadedState 이용
  /* enhancers */
);

ReactDOM.hydrate(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

Dynamic Configuration

서버에선 원하는 기본값 및 설정을 얼마든지 활용해 preloadedState를 만들 수 있으며, 이는 클라이언트 요청에 따라 변하는 동적페이지를 만들 수 있는 기반이 된다.

하지만 아직은 기본값(static configuration)으로 store을 만들고 있으며, 사용자는 요청과 무관하게 매번 새로운 상태를 받고 있다.

이를 해결하기 위해 활용할 수 있는 클라이언트의 HTTP request에는 params, cookies, body 등의 유의미한 정보가 담겨 있으며, 이를 토대로 사용자에 맞는 동적인 store을 만들 수 있다.


SSR Redux Store
<SSR Store - Static vs. Dynamic>


위 다이어그램을 보자. Express 서버에서 request를 활용하여 동적인 preloadedState(dynamic configuration)를 만들고, 이를 토대로 store을 만든다. 이를 이용해 리액트앱을 빌드하여 preloadedState(json)과 함께 HTML에 주입시켜 response로 보낸다.

이렇게 사용자 정보를 토대로 store을 만들 경우 서버는 클라이언트의 활동을 감지하여 리액트앱을 빌드할 수 있는 효과를 얻을 수 있으며, 사용자는 seemless한 UX를 경험할 수 있다.


Async Configuration

해결해야할 문제가 아직 더 남았다. 동적으로 상태를 만드는 것까지는 좋았지만, 비동기 처리는 어떻게 해야할까?

리액트 SSR에서 fetch와 같은 비동기 처리는 생각보다 복잡한 일이다.

이는 ReactDOMServer.renderToString의 동작 방식 때문인데,




Async Configuration

<Async Configuration>







Multiple Components

<Handling SSR state with multiple components>







Redux SSR

<Redux SSR>




Top comments (0)