무엇
hydrateRoot는 서버가 이미 그려 놓은 HTML에 React를 붙이는 API다. 정적인 HTML 스냅샷에 컴포넌트의 로직을 얹어, 브라우저에서 상호작용하는 앱으로 만든다.
첫 인자는 서버가 루트로 그린 domNode, 둘째는 그 HTML을 만들 때 쓴 reactNode(보통 <App />), 셋째는 선택 옵션이다.
import { hydrateRoot } from 'react-dom/client'
hydrateRoot(document.getElementById('root')!, <App />)
돌려받는 root 객체에는 render와 unmount가 있다. 옵션은 createRoot와 같은 에러 콜백들과 identifierPrefix인데, identifierPrefix는 서버에서 쓴 값과 같아야 한다.
무슨 일이 일어나나
이 과정을 하이드레이션이라 한다. React는 HTML을 새로 그리지 않고, 서버가 만든 HTML에 컴포넌트 로직을 "붙인다". 그래서 넘긴 reactNode가 만들어 내는 트리는 서버 HTML과 똑같아야 한다. 다르면 버그로 보고 고쳐야 한다.
서버가 보낸 HTML을 사용자가 먼저 보고 그 위에서 JS가 로드되는데, 트리가 어긋나면 이 연결이 깨진다. 흔한 원인은 React 생성 HTML 주변의 여분 공백, 렌더 로직 안의 typeof window !== 'undefined' 같은 분기, window.matchMedia 같은 브라우저 전용 API, 서버와 클라이언트가 서로 다른 데이터를 그리는 경우다. 개발 모드에서는 불일치를 경고하지만, 속성 차이가 자동으로 메워진다는 보장은 없다.
사용법
클라이언트 진입 파일에서 서버가 그린 컨테이너에 붙인다. 넘기는 컴포넌트는 서버 출력과 일치해야 한다.
import { hydrateRoot } from 'react-dom/client'
import { App } from './App'
// 서버가 이 컨테이너 안 HTML을 이미 그려 두었다
hydrateRoot(document.getElementById('root')!, <App />)
값이 어쩔 수 없이 달라지는 내용(예: 현재 시각)은 suppressHydrationWarning으로 경고만 끈다. 한 단계 깊이에만 적용되는 탈출구라 남용하지 않는다.
<time suppressHydrationWarning>{new Date().toLocaleTimeString()}</time>
실무 예시
에러 처리를 붙여 하이드레이션 중 발생하는 에러를 관측하는 형태다. createRoot와 같은 콜백을 받는다.
import { hydrateRoot } from 'react-dom/client'
import { App } from './App'
const container = document.getElementById('root')
if (!container) throw new Error('root 컨테이너가 없다')
hydrateRoot(container, <App />, {
onCaughtError: (error, info) => {
console.error('경계에서 잡힌 에러:', error, info.componentStack)
},
onRecoverableError: (error) => {
console.warn('복구된 에러:', error)
},
})
왜 중요한가
서버 렌더링(SSR)과 클라이언트를 잇는 다리다. 서버가 보낸 HTML로 첫 화면을 빠르게 보여 주고, 그 위에 상호작용을 얹는 SSR의 마지막 단계가 하이드레이션이다. Next.js 같은 프레임워크가 대신 불러 주므로 직접 쓸 일은 드물지만, 하이드레이션 불일치 경고를 읽고 원인을 짚으려면 이 동작을 알아야 한다.
Reference