React SSR 아키텍처 - Redux Integration
Tags: react, ssr, frontend, redux
Published: 2020-12-29 22:36:01

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

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

<center><br></center>

## preloadedState

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

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

```javascript
// 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>
  `;
}
```

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

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

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

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

[Redux 공식문서](https://redux.js.org/recipes/server-rendering#inject-initial-component-html-and-state)에서는 이 문제를 해결하기 위해 `preloadedState`를 `JSON.stringify`화 시켜 `window` 오브젝트에 주입하는 방법을 알려주고 있다.

```javascript
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>
`;
```

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

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

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

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

<center><br></center>

## Dynamic Configuration

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

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

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

<center>
![SSR Redux Store](https://dev-to-uploads.s3.amazonaws.com/i/m5yonzpx3yn1hmlnvrgs.png)
\<SSR Store - Static vs. Dynamic\>
<br>
</center>

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

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

<center><br></center>

## Async Configuration

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

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

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

<center>
![Async Configuration](https://dev-to-uploads.s3.amazonaws.com/i/yowc1tyzw9yt0t7d5856.png)
\<Async Configuration\>
<br>
</center>

<center>
![Multiple Components](https://dev-to-uploads.s3.amazonaws.com/i/8mnfrn4js08ldvc8efem.png)
\<Handling SSR state with multiple components\>
<br>
</center>

<center>
![Redux SSR](https://dev-to-uploads.s3.amazonaws.com/i/0mqv4c0lgfgu3aqzfvi7.png)
\<Redux SSR\>
<br>
</center>