Next.js Hydration 개념 정리
Hydration이란?
Hydration은 서버에서 렌더링된 정적 HTML에 JavaScript를 연결하여 인터랙티브하게 만드는 과정입니다.
쉽게 말해, “마른 HTML에 물(JavaScript)을 부어 살아있는 React 앱으로 만드는 것”입니다.
렌더링 방식 비교
CSR (Client-Side Rendering)
브라우저 요청 → 빈 HTML 수신 → JS 다운로드 → React 실행 → 화면 렌더링
- 초기 로딩 시 빈 화면 (흰 화면)
- JS가 모두 로드될 때까지 콘텐츠 없음
- SEO 불리
SSR (Server-Side Rendering) + Hydration
브라우저 요청 → 완성된 HTML 수신 → 화면 즉시 표시 → JS 다운로드 → Hydration → 인터랙티브
- 초기 로딩 시 콘텐츠 바로 보임
- SEO 유리
- 단, Hydration 전까지는 버튼 클릭 등 인터랙션 불가
Hydration 과정 상세
[서버]
1. React 컴포넌트를 HTML 문자열로 변환
2. 완성된 HTML을 클라이언트에 전송
[클라이언트]
3. HTML을 파싱하여 화면에 표시 (이 시점에 사용자는 콘텐츠를 볼 수 있음)
4. JavaScript 번들 다운로드
5. React가 기존 HTML에 이벤트 리스너 연결 (Hydration)
6. 이제 완전한 React 앱으로 동작
타임라인 예시
0ms 100ms 300ms 500ms 1000ms
|--------|--------|--------|--------|
HTML 화면 JS Hydration
수신 표시 다운로드 완료
👁️ 볼 수 있음 👆 클릭 가능
Hydration Mismatch 문제
서버에서 렌더링한 HTML과 클라이언트에서 렌더링한 결과가 다르면 Hydration Mismatch 에러가 발생합니다.
발생하는 경우
// ❌ 문제: 서버와 클라이언트의 결과가 다름
function Component() {
return <div>{new Date().toLocaleTimeString()}</div>
// 서버: "오후 2:30:00"
// 클라이언트: "오후 2:30:05" (5초 후)
}
// ❌ 문제: 클라이언트에서만 존재하는 값 사용
function Component() {
return <div>{window.innerWidth}</div>
// 서버: window is not defined 에러
}
// ❌ 문제: 로컬스토리지/쿠키 등 클라이언트 전용 데이터
function Component() {
const user = localStorage.getItem('user')
return <div>{user ? '로그인됨' : '로그아웃'}</div>
// 서버: 항상 '로그아웃' (localStorage 없음)
// 클라이언트: '로그인됨' 또는 '로그아웃'
}
콘솔 에러 메시지
Warning: Text content did not match. Server: "로그아웃" Client: "로그인됨"
Hydration failed because the initial UI does not match what was rendered on the server.
해결 방법
1. useEffect + useState 패턴 (isMounted)
function Component() {
const [isMounted, setIsMounted] = useState(false)
const user = useUserStore() // Zustand 등 클라이언트 상태
useEffect(() => {
setIsMounted(true)
}, [])
// 마운트 전에는 서버와 동일한 결과 반환
if (!isMounted) {
return <div>로딩중...</div>
}
// 마운트 후에는 실제 데이터 사용
return <div>{user ? '로그인됨' : '로그아웃'}</div>
}
2. dynamic import + ssr: false
import dynamic from 'next/dynamic'
// 이 컴포넌트는 서버에서 렌더링되지 않음
const ClientOnlyComponent = dynamic(() => import('./ClientOnlyComponent'), {
ssr: false,
})
function Page() {
return <ClientOnlyComponent />
}
3. suppressHydrationWarning
// 의도적으로 다를 수 있는 경우에만 사용 (시간 표시 등)
function Clock() {
return <time suppressHydrationWarning>{new Date().toLocaleTimeString()}</time>
}
4. useId (React 18+)
import { useId } from 'react'
function Form() {
// 서버와 클라이언트에서 동일한 ID 생성
const id = useId()
return (
<>
<label htmlFor={id}>이름</label>
<input id={id} />
</>
)
}
Next.js App Router에서의 차이
Server Components (기본값)
// app/page.tsx
// 서버에서만 실행됨, Hydration 없음
async function Page() {
const data = await fetch('...')
return <div>{data}</div>
}
Client Components
// app/components/Counter.tsx
'use client' // 이 지시어가 있으면 클라이언트 컴포넌트
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
// 이 컴포넌트는 Hydration 됨
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
정리
| 개념 | 설명 |
|---|---|
| SSR | 서버에서 HTML 생성 |
| Hydration | 서버 HTML에 JS 연결하여 인터랙티브하게 만듦 |
| Hydration Mismatch | 서버/클라이언트 렌더링 결과 불일치 |
| 해결법 | isMounted 패턴, dynamic import, suppressHydrationWarning |