hydrateRoot
는 react-dom/server
를 통해 사전에 만들어진 HTML로 그려진 브라우저 DOM 노드 내부에 React 컴포넌트를 렌더링합니다.
const root = hydrateRoot(domNode, reactNode, options?)
레퍼런스
hydrateRoot(domNode, reactNode, options?)
서버 환경에서 React로 앞서 만들어진 HTML에 후에 만들어진 React를 hydrateRoot
를 호출해 “붙입니다”.
import { hydrateRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = hydrateRoot(domNode, reactNode);
React는 domNode
내부의 HTML에 붙어, 내부 DOM을 직접 관리할 것입니다. App을 React로 전부 만들었다면 보통은 단 하나의 root 컴포넌트와 함께 hydrateRoot
를 한 번 호출할 것입니다.
Parameters
-
domNode
: 서버에서 root 엘리먼트로 렌더링된 DOM 엘리먼트 -
reactNode
: 앞서 존재하는 HTML에 렌더링하기 위한 “React 노드” 입니다. 주로ReactDOM Server
의renderToPipeableStream(<App />)
와 같은 메소드로 렌더링된<App />
같은 JSX 조각들입니다. -
optional
options
: React root에 옵션을 주기 위한 객체입니다.-
Canary only optional
onCaughtError
: Callback called when React catches an error in an Error Boundary. Called with theerror
caught by the Error Boundary, and anerrorInfo
object containing thecomponentStack
. -
Canary only optional
onUncaughtError
: Callback called when an error is thrown and not caught by an Error Boundary. Called with theerror
that was thrown and anerrorInfo
object containing thecomponentStack
. -
optional
onRecoverableError
: Callback called when React automatically recovers from errors. Called with theerror
React throws, and anerrorInfo
object containing thecomponentStack
. Some recoverable errors may include the original error cause aserror.cause
. -
optional
identifierPrefix
: A string prefix React uses for IDs generated byuseId
. Useful to avoid conflicts when using multiple roots on the same page. Must be the same prefix as used on the server. -
optional
onRecoverableError
: React가 에러에서 자동으로 회복되었을 때 호출하는 콜백함수. -
optional
identifierPrefix
: React가 ID로 사용하는 접두사로useId
로 만들어진 값입니다. 같은 페이지에서 여러 root를 사용할 때 충돌을 피할 때 유용하게 사용할 수 있습니다. 서버에서 사용한 값과 반드시 동일한 값이어야 합니다.
-
Returns
hydrateRoot
는 2가지 메소드가 포함된 객체를 반환합니다 : render
그리고 unmount
.
주의 사항
hydrateRoot()
는 서버에서 렌더링된 내용과 후에 렌더링된 내용이 동일할 것을 기대합니다. 따라서 동일하지 않은 부분들은 직접 버그로 취급해주거나 고쳐줘야 합니다.- 개발 모드에선, React가 hydration 중에 동일하지 않은 부분에 대해 경고해줍니다. 속성이 동일하지 않을 경우에 해당 속성이 올바르게 적용될 것이라고 보장할 수 없습니다. 모든 마크업을 보장하지 않는 것은 성능면에서 중요하기 때문입니다. 마크업이 동일하지 않는 경우는 드물기 때문에 모든 마크업을 검증하는 비용은 굉장히 비쌉니다.
- 여러분은 App에서
hydrateRoot
를 단 한 번만 호출하게 될 것입니다. 만약 프레임워크를 사용한다면, 프레임워크가 대신 호출해 줄 것입니다. - App을 사전에 렌더링된 HTML 없이 클라이언트에서 직접 렌더링을 한다면
hydrateRoot()
은 지원되지 않습니다.createRoot()
를 대신 사용해주세요.
root.render(reactNode)
hydrate된 React root부터 내부 컴포넌트를 새로운 React 컴포넌트로 갱신하기 위해 root.render
를 호출해주세요. 브라우저 DOM 요소들도 함께 갱신됩니다.
root.render(<App />);
React는 hydrate된 root
부터 내부를 <App />
으로 갱신합니다.
Parameters
reactNode
: 갱신하고 싶은 “React 노드”입니다. 주로<App />
같은 JSX를 매개변수로 넘기지만,createElement()
로 만든 React 엘리먼트를 넘겨도 되고 문자열이나 숫자,null
, 혹은undefined
를 넘겨도 됩니다.
Returns
root.render
는 undefined
를 반환합니다.
주의 사항
- hydrate가 끝나기 전에
root.render
를 호출하면 React는 서버에서 렌더링된 HTML을 모두 없애고 클라이언트에서 렌더링된 컴포넌트들로 완전히 교체합니다.
root.unmount()
root.unmount
를 호출해 React root부터 그 하위에 렌더링된 트리를 삭제합니다.
root.unmount();
처음부터 끝까지 React로 만든 앱은 root.unmount
를 호출할 경우가 거의 없습니다.
주로 React root부터 혹은 그 상위에서부터 시작된 DOM node들을 다른 코드에 의해 DOM에서 삭제되어야 하는 경우 유용합니다. 예를 들어, jQuery 탭 패널이 활성화 되어 있지 않은 탭을 DOM에서 지운다고 가정해봅시다. 탭이 지워지면, React root와 그 내부를 포함해 그 안의 모든 것이 지워지게 되고 DOM에서 또한 지워지게 됩니다. root.unmount
를 호출해 React에게 삭제된 컨텐츠들을 “그만” 다루라고 알려주어야 합니다. 그렇지 않으면 삭제되어버린 React root 내부의 컴포넌트들은 삭제되지 않을 것이며, “구독”처럼 컴퓨팅 자원을 자유롭게 놓아주지 못하게 됩니다.
root.unmount
를 호출하면 root 내부의 모든 컴포넌트를 마운트 해제하고 root DOM node에서 React를 “떼어”냅니다. root 내부의 이벤트 핸들러와 state까지 모두 포함해 마운트 해제 및 삭제됩니다.
Parameters
root.unmount
는 그 어떤 매개변수도 받지 않습니다.
Returns
root.unmount
returns undefined
.
주의 사항
-
root.unmount
를 호출하면 root부터 그 안의 모든 컴포넌트가 마운트 해제되고 root DOM node에서 React를 떼어냅니다. -
root.unmount
를 한번 호출한 이후엔root.render
를 root에 다시 사용할 수 없습니다. 마운트 해제된 root에 다시root.render
를 호출하려고 한다면 “Cannot update an unmounted root” 에러를 throw하게 됩니다.
사용 예시
서버에서 렌더링된 HTML을 hydrate하기
react-dom/server
로 앱의 HTML을 생성했다면, 클라이언트에서 hydrate해주어야 합니다.
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
위 코드를 통해 서버 HTML을 브라우저 DOM node에서 React 컴포넌트를 이용해 hydrate 해줄 것 입니다. 주로 앱을 시작할 때 단 한 번 실행하게 될 것입니다. 프레임워크를 사용중이라면 프레임워크가 알아서 실행해 줄 것입니다.
앱을 hydrate하기 위해서 React는 컴포넌트의 로직을 사전에 서버에서 만들어 진 HTML에 “붙일”것 입니다. Hydration을 통해 서버에서 만들어진 최초의 HTML 스냅샷을 브라우저에서 완전히 인터랙티브한 앱으로 바꿔주게 됩니다.
import './styles.css'; import { hydrateRoot } from 'react-dom/client'; import App from './App.js'; hydrateRoot( document.getElementById('root'), <App /> );
hydrateRoot
를 다시 호출하거나 다른 곳에서 더 호출할 필요는 없습니다. 이 시점부터 React가 애플리케이션의 DOM을 다루게 됩니다. 대신 UI를 갱신하기 위해선 state를 사용해야 합니다.
document 전체를 hydrate하기
React로 앱을 모두 만들었을 경우 <html>
태그를 포함해 JSX로 된 전체 document를 렌더링할 수 있습니다.
function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}
전체 document를 hydrate하기 위해선 글로벌 변수인 document
를 hydrateRoot
의 첫번째 인자로 넘깁니다:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
어쩔 수 없는 hydration 불일치 에러 억제하기
어떤 엘리먼트의 속성이나 text content가 서버와 클라이언트에서 어쩔 수 없이 다를 땐(예를 들어, timestamp를 이용했다거나), hydration 불일치 경고를 안보이게 할 수 있습니다.
해당 엘리먼트에서 hydration 경고를 끄기 위해선 suppressHydrationWarning={true}
를 추가하면 됩니다.
export default function App() { return ( <h1 suppressHydrationWarning={true}> Current Date: {new Date().toLocaleDateString()} </h1> ); }
이것은 한 단계 아래까지만 적용되며 비상 탈출구를 의도한 것입니다. 남용하지 마세요. text context가 아닌 한, React는 잘못된 부분을 수정하지 않을 것이며 갱신이 일어나기 전까지는 불일치한 상태로 남아있을 것입니다.
서로 다른 클라이언트와 서버 컨텐츠 다루기
의도적으로 서버와 클라이언트에서 서로 다른 내용을 렌더링하길 원한다면, 서버와 클라이언트에서 서로 다른 방법으로 렌더링하면 됩니다. 클라이언트에서 서버와는 다른 것을 렌더링할 때 클라이언트에선 Effect에서 true
로 할당되는 isClient
같은 상태 변수를 읽을 수 있습니다.
import { useState, useEffect } from "react"; export default function App() { const [isClient, setIsClient] = useState(false); useEffect(() => { setIsClient(true); }, []); return ( <h1> {isClient ? 'Is Client' : 'Is Server'} </h1> ); }
이 방법은 처음엔 서버와 동일한 결과물을 렌더링하게 되어 불일치 문제를 피하게 되고, hydration후에 새로운 결과물이 동기적으로 렌더링됩니다.
hydrate된 root 컴포넌트를 업데이트하기
root의 hydrating이 끝난 이후에, root.render
를 호출해 React 컴포넌트의 root를 업데이트 할 수 있습니다. createRoot
와는 다르게 HTML로 최초의 컨텐츠가 이미 렌더링 되어 있기 때문에 자주 사용할 필요는 없습니다.
hydration 후 어떤 시점에 root.render
를 호출한다면, 그리고 컴포넌트의 트리 구조가 이전에 렌더링했던 구조와 일치한다면, React는 상태를 그대로 보존합니다. input에 어떻게 타이핑하는지에 따라 문제가 발생하지 않습니다. 즉, 아래 예시에서처럼 매초 마다 상태를 업데이트하는 반복적인 render
는 문제 없이 렌더링 된다는 것을 볼 수 있습니다:
import { hydrateRoot } from 'react-dom/client'; import './styles.css'; import App from './App.js'; const root = hydrateRoot( document.getElementById('root'), <App counter={0} /> ); let i = 0; setInterval(() => { root.render(<App counter={i} />); i++; }, 1000);
hydrate된 root에서 root.render
를 호출하는 것은 흔한 일은 아닙니다. 내부 컴포넌트 중 한 곳에서 useState를 하는 것이 일반적입니다.
처리되지 않은 에러에 대한 대화 상자 표시하기
기본적으로 React는 처리되지 않은 모든 에러를 콘솔에 기록합니다. 자체적인 에러 보고 기능을 구현하려면 선택적 root 옵션인 onUncaughtError
를 사용할 수 있습니다.
import { hydrateRoot } from 'react-dom/client';
const root = hydrateRoot(
document.getElementById('root'),
<App />,
{
onUncaughtError: (error, errorInfo) => {
console.error(
'Uncaught error',
error,
errorInfo.componentStack
);
}
}
);
root.render(<App />);
onUncaughtError 옵션은 두 개의 인자를 받는 함수입니다.
- 발생한 에러.
- 에러의 componentStack을 포함하는 errorInfo 객체.
onUncaughtError
root 옵션을 사용해 에러 대화 상자를 표시할 수 있습니다.
import { hydrateRoot } from "react-dom/client"; import App from "./App.js"; import {reportUncaughtError} from "./reportError"; import "./styles.css"; import {renderToString} from 'react-dom/server'; const container = document.getElementById("root"); const root = hydrateRoot(container, <App />, { onUncaughtError: (error, errorInfo) => { if (error.message !== 'Known error') { reportUncaughtError({ error, componentStack: errorInfo.componentStack }); } } });
Error Boundary 에러 표시하기
기본적으로 React는 Error Boundary에 의해 잡힌 모든 에러를 console.error
에 기록합니다. 이 동작을 재정의하려면 Error Boundary에서 잡힌 에러 처리에 대한 선택적 root 옵션인 onCaughtError
를 사용할 수 있습니다.
import { hydrateRoot } from 'react-dom/client';
const root = hydrateRoot(
document.getElementById('root'),
<App />,
{
onCaughtError: (error, errorInfo) => {
console.error(
'Caught error',
error,
errorInfo.componentStack
);
}
}
);
root.render(<App />);
onCaughtError 옵션은 두 개의 인수를 받는 함수입니다.
- Error Boundary에 의해 잡힌 에러.
- 에러의 componentStack을 포함하는 errorInfo.
onCaughtError
root 옵션을 사용해 에러 대화 상자를 표시하거나 기록된 에러 중 알고 있는 에러를 필터링할 수 있습니다.
import { hydrateRoot } from "react-dom/client"; import App from "./App.js"; import {reportCaughtError} from "./reportError"; import "./styles.css"; const container = document.getElementById("root"); const root = hydrateRoot(container, <App />, { onCaughtError: (error, errorInfo) => { if (error.message !== 'Known error') { reportCaughtError({ error, componentStack: errorInfo.componentStack }); } } });
복구 가능한 hydration 불일치 에러에 대한 대화 상자 표시하기
React가 hydration 불일치를 만나면 클라이언트에서 자동으로 렌더링을 시도합니다. 기본적으로 React는 hydration 불일치 에러를 console.error
에 기록합니다. 이 동작을 재정의하려면 선택적 root 옵션인 onRecoverableError
를 사용할 수 있습니다.
import { hydrateRoot } from 'react-dom/client';
const root = hydrateRoot(
document.getElementById('root'),
<App />,
{
onRecoverableError: (error, errorInfo) => {
console.error(
'Caught error',
error,
error.cause,
errorInfo.componentStack
);
}
}
);
onRecoverableError 옵션은 두 개의 인자를 받는 함수입니다.
- React가 발생시킨 error. 일부 에러는 원래 원인을 error.cause에 포함하기도 합니다.
- 에러의 componentStack을 포함하는 errorInfo 객체.
hydration 불일치에 대한 대화 상자를 표시하려면 onRecoverableError
root 옵션을 사용할 수 있습니다.
import { hydrateRoot } from "react-dom/client"; import App from "./App.js"; import {reportRecoverableError} from "./reportError"; import "./styles.css"; const container = document.getElementById("root"); const root = hydrateRoot(container, <App />, { onRecoverableError: (error, errorInfo) => { reportRecoverableError({ error, cause: error.cause, componentStack: errorInfo.componentStack }); } });
문제 해결
다음과 같은 에러가 발생합니다: “You passed a second argument to root.render”
hydrateRoot
옵션을 root.render(...)
에 전달하는 실수가 흔히 일어나곤 합니다.
root.render(...)
에 두 번째 인자를 전달했지만, 이 함수는 하나의 인자만 허용합니다.수정하려면 root 옵션을 root.render(...)
대신 hydrateRoot(...)
에 전달하세요.
// 🚩 잘못된 방법: root.render는 하나의 인자만 받습니다.
root.render(App, {onUncaughtError});
// ✅ 올바른 방법: 옵션을 createRoot에 전달하세요.
const root = hydrateRoot(container, <App />, {onUncaughtError});