SSR Hydration Dive
Hydration
hydration이 뭘까? 위키에 쳐보니 화학 이야기가 나온다 간단하게 무엇에 스며드는 것을 말하는 것 같다. NextJS에서는 hydration 개념이 중요한데 NextJS에서 말하는 hydration은 뭘까? 이는 서버 사이드 렌더링(SSR)과 관련이 있다.

SSR를 사용할 경우 서버에서 만든 HTML를 클라이언트에게 전송하고, 클라이언트에서 추가적으로 JavaScript를 Load해서 페이지와 User가 상호작용을 할 수 있다.
이 과정에서 정적 HTML과 동적 React Component를 연결하는 과정이 하이드레이션이다. 이와 관련 된 함수로는 ReactDOM.hydrate 가 있다. ReactDOM.hydrate 함수는 ReactDOM.render 와 함께 많이 비교되는데 둘 다 알아보자!
ReactDOM.render
render 함수는 클라이언트 사이드 렌더링(CSR)을 수행하는 API이다. 리액트를 사용해봤다면 아래 코드를 본 적 있을 것이다.
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from './src/App';
// ...
ReactDOM.render(<App />, document.getElementById("root"));
render 함수는 클라이언트에서 전체 트리 구조를 생성하고 렌더링하는 데 사용되며, DOM 요소를 리액트 컴포넌트와 바인딩하는 데에도 쓰인다.
ReactDOM.render(element, container, [callback]);
// element: 컴포넌트
// container: 지정할 DOM 요소
// callback: 렌더링이 완료되면 실행할 콜백
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
if (__DEV__) {
console.error(
'ReactDOM.render is no longer supported in React 18. Use createRoot ' +
'instead. Until you switch to the new API, your app will behave as ' +
"if it's running React 17. Learn " +
'more: https://reactjs.org/link/switch-to-createroot',
);
}
if (!isValidContainerLegacy(container)) {
throw new Error('Target container is not a DOM element.');
}
if (__DEV__) {
const isModernRoot =
isContainerMarkedAsRoot(container) &&
container._reactRootContainer === undefined;
if (isModernRoot) {
console.error(
'You are calling ReactDOM.render() on a container that was previously ' +
'passed to ReactDOMClient.createRoot(). This is not supported. ' +
'Did you mean to call root.render(element)?',
);
}
}
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
위 코드는 React Repository에서 가져왔다. 18 버전 이상에서는 render 대신 createRoot 함수를 쓰기 때문에 그에 대한 경고 메시지를 출력한다. isValidContainerLegacy 로 DOM 요소인지 확인하고 개발 모드에서 컨테이너가 이전에 ReactDOMClient.createRoot()로 전달되었는지 확인하는 추가 검증 후 모든 검증이 완료되면 legacyRenderSubtreeIntoContainer 를 호출한다.
legacyRenderSubtreeIntoContainer
legacyRenderSubtreeIntoContainer 구현부
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
if (__DEV__) {
topLevelUpdateWarnings(container);
warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
}
const maybeRoot = container._reactRootContainer;
let root: FiberRoot;
if (!maybeRoot) {
// Initial mount
root = legacyCreateRootFromDOMContainer(
container,
children,
parentComponent,
callback,
forceHydrate,
);
} else {
root = maybeRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function () {
const instance = getPublicRootInstance(root);
originalCallback.call(instance);
};
}
// Update
updateContainer(children, root, parentComponent, callback);
}
return getPublicRootInstance(root);
}
간단 요약
- 실제 DOM에 대응하는 리액트 루트를 생성
- 전달된 리액트 컴포넌트를 렌더링하거나 업데이트
- 최상위 컴포넌트 또는 React 공용 인스턴스를 반환
다음은 SSR을 사용할 때 사용하는 ReactDOM.hydrate 에 대해 알아보자!
ReactDOM.hydrate
SSR된 APP을 클라이언트에서 다시 렌더링할 때 사용
export function hydrate(
element: React$Node,
container: Container,
callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
if (__DEV__) {
console.error(
'ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot ' +
'instead. Until you switch to the new API, your app will behave as ' +
"if it's running React 17. Learn " +
'more: https://reactjs.org/link/switch-to-createroot',
);
}
if (!isValidContainerLegacy(container)) {
throw new Error('Target container is not a DOM element.');
}
if (__DEV__) {
const isModernRoot =
isContainerMarkedAsRoot(container) &&
container._reactRootContainer === undefined;
if (isModernRoot) {
console.error(
'You are calling ReactDOM.hydrate() on a container that was previously ' +
'passed to ReactDOMClient.createRoot(). This is not supported. ' +
'Did you mean to call hydrateRoot(container, element)?',
);
}
}
// TODO: throw or warn if we couldn't hydrate?
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback,
);
}
render 함수와 비교해보면 코드가 거의 같다. legacyRenderSubtreeIntoContainer 의 4번째 Param에 false, true 중 뭘 넣는 지 차이이다.
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
) {
// ...
root = legacyCreateRootFromDOMContainer(
container,
children,
parentComponent,
callback,
forceHydrate,
);
// ...
}
해당 Param은 다시 legacyCreateRootFromDOMContainer 함수에 Param으로 들어간다.
legacyCreateRootFromDOMContainer
legacyCreateRootFromDOMContainer 는 리액트 App의 루트를 실제 DOM에 연결하는 역할을 한다.
legacyCreateRootFromDOMContainer 구현부
function legacyCreateRootFromDOMContainer(
container: Container,
initialChildren: ReactNodeList,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
isHydrationContainer: boolean,
): FiberRoot {
if (isHydrationContainer) {
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function () {
const instance = getPublicRootInstance(root);
originalCallback.call(instance);
};
}
const root: FiberRoot = createHydrationContainer(
initialChildren,
callback,
container,
LegacyRoot,
null, // hydrationCallbacks
false, // isStrictMode
false, // concurrentUpdatesByDefaultOverride,
'', // identifierPrefix
noopOnRecoverableError,
// TODO(luna) Support hydration later
null,
);
container._reactRootContainer = root;
markContainerAsRoot(root.current, container);
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
// $FlowFixMe[incompatible-call]
listenToAllSupportedEvents(rootContainerElement);
flushSync();
return root;
} else {
// First clear any existing content.
clearContainer(container);
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function () {
const instance = getPublicRootInstance(root);
originalCallback.call(instance);
};
}
const root = createContainer(
container,
LegacyRoot,
null, // hydrationCallbacks
false, // isStrictMode
false, // concurrentUpdatesByDefaultOverride,
'', // identifierPrefix
noopOnRecoverableError, // onRecoverableError
null, // transitionCallbacks
);
container._reactRootContainer = root;
markContainerAsRoot(root.current, container);
const rootContainerElement =
container.nodeType === COMMENT_NODE ? container.parentNode : container;
// $FlowFixMe[incompatible-call]
listenToAllSupportedEvents(rootContainerElement);
// Initial mount should not be batched.
flushSync(() => {
updateContainer(initialChildren, root, parentComponent, callback);
});
return root;
}
}
순서
- 하이드레이션 사용 유무 결정
- 리액트 컨테이너를 생성하고, 필요할 때 하이드레이션을 수행함
- 이벤트 리스너를 연결
forceHydrate(isHydrationContainer) 를 사용하는 부분만 아래에서 간단히 설명해본다.
isHydrationContainer 가 true 인 경우
- 콜백 함수 설정 (있다면)
- 하이드레이션 컨테이너를 생성하고 해당 컨테이너를 리액트 루트 컨테이너로 등록
- 모든 이벤트를 컨테이너에 연결
- flushSync 로 동기적 실행하며 리렌더함
isHydrationContainer 가 false 인 경우
- 기존 컨테이너 삭제
- 콜백 함수 설정 (있다면)
- 일반 컨테이너를 생성하고 해당 컨테이너를 리액트 루트 컨테이너로 등록
- 모든 이벤트를 컨테이너에 연결
- flushSync 에 콜백을 주어 먼저 대기중인 동기 작업(랜더링 등)을 한 후 콜백을 실행한다.
결국 돌고 돌아 SSR인 경우는 하이드레이션 컨테이너 사용하고 CSR인 경우는 일반 컨테이너를 사용한다.
createHydrationContainer 로 생성된 컨테이너는 정적 HTML을 동적으로 처리할 수 있는 React 컴포넌트 트리로 교체되고, createContainer 로 생성된 컨테이너는 새로운 React 컴포넌트 트리를 만들 때 사용되며 서버 사이드에 렌더링된 정적 HTML 없이 작동한다.
React 18
하이드레이션과 큰 연관은 없지만 위에서 설명한 ReactDOM.render 와 ReactDOM.hydrate 는 18 버젼 미만에서 사용하였고 18 버젼 이상에서는 createRoot 와 hydrateRoot 를 사용하기 때문에 두 함수를 간단히 알아보자!
createRoot
createRoot 와 render 의 주된 차이는 렌더링 전략과 작업 우선순위 처리 방식에 있다. render 함수는 동기적으로 컴포넌트를 DOM에 마운트하고 렌더링한다. 이는 동기적으로 실행하기 때문에 성능 저하를 초래할 수도 있다. 이에 대한 해결을 위해 createRoot 함수는 Concurrent Mode 로 렌더링을 한다. Concurrent Mode 는 작업이 비동기적으로 처리되며 각 업데이트가 동시에 실행된다.
import ReactDOM from 'react-dom';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
hydrateRoot
hydrateRoot 함수도 마찬가지로 Concurrent Mode 를 활용해서 동시에 업데이트를 하면서 하이드레이션를 최적화한다고 하는데 createRoot 와 hydrateRoot 는 나중에 한번 파도록하고 이번에는 장점만 알아보자.
Reference
Next.js의 렌더링 과정(Hydrate) 알아보기
누군가 나에게 Next.js를 쓰는 이유를 물어본다면, 가장 먼저 SSR 때문이라고 대답할 것 같다. Next.js 공식 홈페이지에서도 가장 먼저 강조하고 있는 것이 'hybrid static & server rendering'인 것처럼 말이다. 하지만 정확히 어떠...

Hydrate(Next, React 18)
CSR과 SS에 대해 알아보고 Hydrate에 대한 개념을 살펴보자. 그리고 React 18에서 추가된 Suspense의 강력한 기능과 Hydrate를 알아보자.
