리액트에서 공식적으로 지원하는 ReactDOM.hydrate
및 ReactDOMServer.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>
`;
}
문제의 해결 방법은 꽤나 직관적이다 - 서버에서 초기상태(preloadedState
)를 관리하여 store
을 만들어주면 된다.
이렇게 하면 store
을 기반으로 리액트앱이 빌드되고, 위처럼 content
를 통해 HTML에 주입된다.
하지만 역시나 문제가 생긴다. 바로 이 preloadedState
가 클라이언트에게 없다는 것이다. preloadedState
를 이용해 리액트앱을 서버에서 빌드하여 클라이언트에게 보내는 것까진 괜찮지만, 정작 클라이언트는 '상태'는 받고있지 않다.
클라이언트에 preloadedState
가 없다면 서버와 클라이언트의 상태가 다르다는 것을 뜻하며, 상태가 다르니 만들어지는 리액트앱도 다르다. 즉, hydration 과정에 문제가 생긴다.
Redux 공식문서에서는 이 문제를 해결하기 위해 preloadedState
를 JSON.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>
`;
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')
);
Dynamic Configuration
서버에선 원하는 기본값 및 설정을 얼마든지 활용해 preloadedState
를 만들 수 있으며, 이는 클라이언트 요청에 따라 변하는 동적페이지를 만들 수 있는 기반이 된다.
하지만 아직은 기본값(static configuration)으로 store
을 만들고 있으며, 사용자는 요청과 무관하게 매번 새로운 상태를 받고 있다.
이를 해결하기 위해 활용할 수 있는 클라이언트의 HTTP request
에는 params
, cookies
, body
등의 유의미한 정보가 담겨 있으며, 이를 토대로 사용자에 맞는 동적인 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>
<Handling SSR state with multiple components>
<Redux SSR>
Top comments (0)