React 16 이후, React는 “Fiber”라는 내부 구조를 도입했습니다.
이는 컴포넌트 트리를 표현하는 메모리 구조체(virtual tree)이며, 하이드레이션은 바로 이 Fiber 트리를 “서버에서 미리 만들어진 DOM 노드”와 동기화하는 과정입니다.
SSR에서 HTML은 이미 렌더링되어 있으므로, 클라이언트는 새로 만드는 대신 기존 노드에 Fiber를 연결(hydrate)해야 함.
하이드레이션은 ReactDOM.hydrateRoot()(React 18 이후 기준)로 시작합니다.
ReactDOM.hydrateRoot(container, element)
이 호출은 대략 다음 6단계를 거칩니다.
| 단계 | 이름 | 핵심 동작 | 관련 코드 경로 (react-dom 내부) |
|---|---|---|---|
| 1️⃣ | Container 초기화 | 서버에서 받은 HTML 루트 찾기 | ReactDOMRoot.createRootImpl() |
| 2️⃣ | Fiber Tree 생성 | Virtual Fiber Tree 구성 | createFiberFromTypeAndProps() |
| 3️⃣ | Hydration Queue 생성 | 미리 존재하는 DOM과 매칭 준비 | prepareToHydrateHostInstance() |
| 4️⃣ | Reconciliation | Fiber와 기존 DOM을 비교(diff 없이 attach) | hydrateInstance(), hydrateTextInstance() |
| 5️⃣ | Commit Phase | 이벤트 핸들러, ref, effect 복원 | commitRoot() |
| 6️⃣ | Post-hydration Update | useEffect, useLayoutEffect 실행 | flushPassiveEffects() |

목표: SSR에서 생성된 루트 DOM 노드를 찾고, 하이드레이션 모드로 렌더링 준비.
const root = ReactDOM.hydrateRoot(document.getElementById('root'), <App />);
이 시점에서 React는
document.getElementById('root') 아래의 모든 노드가 “SSR로 렌더링된 HTML”임을 가정합니다.<App />을 렌더링하기 위한 Fiber Root 객체를 만듭니다.이 플래그는 React 내부에서 “새로 DOM을 만들지 말고, 기존 것을 재활용하라”는 신호로 작동합니다.
React는 App 컴포넌트를 재귀적으로 순회하며 Fiber Tree를 생성합니다.
| 필드 | 의미 |
|---|---|
| type | 함수/클래스/HTML 태그 |
| pendingProps | 렌더링 시점의 props |
| stateNode | 실제 DOM 노드 참조 |
| return | 부모 Fiber |
| child / sibling | 트리 구조 |
| flags | update / placement / hydration 등 상태 |
이 Fiber Tree는 아직 실제 DOM과 연결되지 않은 가상 구조에 불과합니다.
이제 React는 “hydration queue”라는 내부 구조를 만들어서 SSR에서 이미 존재하는 DOM 노드들을 순차적으로 읽어가기 시작합니다.
📦 핵심 함수:
prepareToHydrateHostInstance()→ DOM 노드를 Fiber와 매칭할 준비를 함.
이 시점에서는 아직 이벤트 핸들러도, ref도, state도 연결되지 않았습니다.
그냥 HTML element의 형태와 순서를 검증 중이에요.
이제 진짜 하이드레이션이 일어납니다.
서버에서 만들어진 DOM과 클라이언트의 Virtual Fiber를 하나씩 대응시킵니다.
React는 각 Fiber의 type과 기존 DOM 노드의 nodeName을 비교
→ <div> Fiber는 <div> DOM과 연결
→ <span> Fiber는 <span> DOM과 연결
→ 불일치하면 “hydration mismatch warning” 출력
TextNode인 경우:
→ SSR HTML의 텍스트와 props.children 문자열을 비교
→ 다르면 hydrateTextInstance()에서 mismatch 처리
📦 내부 주요 함수:
hydrateInstance(fiber, domNode); hydrateTextInstance(fiber, textNode);
즉, 이 단계에서 React는 기존 DOM을 파괴하지 않고 “붙이기만” 합니다.
하이드레이션의 마지막 핵심 단계입니다.
이제 DOM과 Fiber가 매칭되었으니, React는 “활성화”를 시작합니다.
📦 관련 코드:
commitRoot(root); commitMount();
이제 이 시점부터, 클라이언트는 기존 HTML을 “React 트리”로 완전히 제어할 수 있게 됩니다.
즉, “hydrated” 상태가 된 거예요.
이제 useEffect()나 componentDidMount() 같은 비동기 후처리 로직이 실행됩니다.
flushPassiveEffects()를 통해 모든 side-effect를 수행이제 화면이 완전히 살아나고, 유저 이벤트를 받을 준비가 끝납니다. 🎉

아래와 같은 경우에 mismatch 경고가 발생합니다.
| 원인 | 예시 | 결과 |
|---|---|---|
| 서버와 클라이언트의 랜덤 값 차이 | <div>{Math.random()}</div> | DOM 리렌더링 |
| 날짜, 시간 값 다름 | <div>{new Date().toLocaleTimeString()}</div> | mismatch 경고 |
| 조건부 렌더링 불일치 | 서버는 A, 클라이언트는 B 렌더링 | fallback to client render |
React는 mismatch를 발견하면 “soft fallback”으로 전환합니다.
→ 기존 DOM을 버리고 다시 렌더링함.
즉, 다시 복구되지만 다른 버그들과 같이 반드시 고쳐줘야 합니다. 가장 나은 경우는 그저 느려지기만 할 뿐이지만, 최악의 경우엔 이벤트 핸들러가 다른 요소(Element)에 붙어버립니다.
React 18은 하이드레이션 전체를 병렬 처리할 수 있도록 개선했습니다.
📦 내부적으로는
ReactDOMServer.stream()→hydrateRoot()간의 “token stream”으로 연결됨.
즉, Hydration이 더 이상 전체 페이지 단위가 아니라, 트리 일부 단위로 분산 수행됩니다.
(SSR) HTML 전달
↓
[Step 1] hydrateRoot() 호출
↓
[Step 2] Fiber Tree 생성
↓
[Step 3] DOM과 Fiber 매칭 (hydrateInstance)
↓
[Step 4] 이벤트/Ref 복원
↓
[Step 5] useEffect 실행
↓
🎉 페이지 완전 활성화
| 개념 | 설명 |
|---|---|
| Hydration | SSR로 만들어진 HTML에 Fiber 트리를 연결해 JS 이벤트/상태를 복원하는 과정 |
| Fiber Tree | React 내부에서 컴포넌트 구조를 메모리로 표현하는 트리 구조 |
| Hydration Queue | 기존 DOM을 순회하면서 Fiber를 하나씩 attach하는 대기열 |
| Selective Hydration | Suspense boundary 단위로 병렬/부분 하이드레이션 |
| Mismatch | 서버와 클라이언트 렌더 결과가 다를 때 발생 (React가 DOM 재생성으로 대응) |