정의
하이드레이션 불일치(hydration mismatch)는 서버가 렌더한 HTML과 클라이언트의 첫 번째 렌더 결과가 달라서, 프레임워크가 기존 DOM에 이벤트 핸들러를 재부착(hydrate)하는 과정에서 트리를 맞추지 못하는 상황이다. 첫 렌더에서 서버에는 없는 값(localStorage, window, 랜덤, 현재 시각)을 사용할 때 전형적으로 발생한다.
왜 필요한가
SSR(서버 사이드 렌더링: 요청마다 서버가 HTML을 만들어 보냄)/SSG(정적 사이트 생성: 빌드 시점에 HTML을 미리 만들어 둠)는 HTML을 서버에서 먼저 만들어 보내 사용자가 빠르게 콘텐츠를 보게 하고(critical-rendering-path), 브라우저에서 같은 트리를 React가 재부착(hydrate)해 상호작용을 입힌다. 이 "서버 마크업 == 첫 클라이언트 렌더" 전제가 깨지면 경고가 뜨고, 깜빡임(flicker)이나 DOM 폐기·재생성이 일어나 성능과 시각 안정성이 나빠진다. 따라서 클라이언트 전용 상태를 언제 반영하느냐가 핵심이다.
동작 원리
서버에는 window·localStorage가 없다. 그래서 컴포넌트가 첫 렌더에서 이런 값을 읽으면, 서버는 기본값으로, 클라이언트는 저장된 값으로 렌더해 둘이 갈린다. React는 hydrate 시 두 트리를 비교하다 차이를 발견하고 경고한다.
| 전략 | 방법 | 트레이드오프 |
|---|---|---|
| 마운트 후 1회 반영 | 첫 렌더는 SSR과 동일한 기본값, useEffect에서 클라이언트 값 적용 | 한 프레임 깜빡임 가능 |
| 동기 인라인 스크립트 | hydrate 전에 inline <script>가 DOM 속성을 직접 세팅 | 깜빡임 없음, 보일러플레이트 증가 |
| 의도된 불일치 억제 | 시각 등 본질적으로 다른 값만 suppressHydrationWarning | 진짜 버그를 가리면 안 됨 |
실무 적용
깜빡임까지 없애야 하면, hydrate 전에 실행되는 인라인 스크립트로 DOM 속성을 직접 세팅한 뒤 React가 그 위에 재부착하도록 한다. 영속 레이아웃 값(localStorage 등)은 스토어 초기화 시점이 아니라 마운트 이후에 읽는 것이 기본 규칙이다.
트레이드오프
- 마운트 후 반영은 구현이 단순하지만 기본값 → 실제값 전환에서 한 프레임 깜빡임이 보일 수 있다.
- 인라인 스크립트는 깜빡임이 없지만 코드가 늘고, 스크립트와 React 상태를 어긋나지 않게 관리해야 한다.
사용하면 안 되는 경우
- CSR 전용 앱(서버 렌더가 없으면 불일치 자체가 없다).
- 값이 서버에서도 결정 가능한 경우 — 굳이 클라이언트 전용으로 미룰 필요 없이 서버에서 렌더한다.
흔한 실수
- 첫 렌더 본문에서
localStorage/window/Date.now()/랜덤을 직접 사용한다. suppressHydrationWarning을 광범위하게 붙여 진짜 불일치 버그를 가린다.- 스토어를
localStorage로 초기화해(첫 렌더부터 클라이언트 값) 서버와 어긋난다.
관련 개념
- critical-rendering-path — 서버가 만든 HTML이 먼저 그려지고 이후 JS가 재부착되는 렌더링 순서.