본문으로 건너뛰기
yutils

hydration 은 어떻게 동작할까?

React 가 SSR HTML 위에 event handler 부착, hydration mismatch 에러의 진짜 원인, progressive / selective / partial hydration, island architecture (Astro / Qwik resumability), hydration 이 SSR 의 cost 인 이유.

약 9분 읽기

SSR 의 빠른 first paint + SPA 의 interactivity 결합. 그러나 무료 아님 — hydration 이라는 cost. server 가 만든 HTML 위에 React 가 event handler 부착하는 단계. mismatch error, progressive hydration, Astro / Qwik 의 island architecture — 이 가이드는 모두 정리한다.

Hydration — 무엇을 하나

SSR 의 흐름:
1. server: React tree → HTML 문자열
2. browser: HTML 즉시 paint (사용자가 봄, 빠름)
3. browser: JS bundle download
4. hydration:
   - server HTML 의 각 element 에 React fiber 매핑
   - event handler (onClick 등) 부착
   - useState / useEffect 활성
   - "이제 interactive"

그 사이 (3 → 4 끝나기 전):
   - UI 는 보이지만 클릭 등 반응 X (또는 부분 반응)
   - "First Contentful Paint" 빠름 + "Time to Interactive" 늦음의 gap

→ hydration cost = SSR 의 trade-off.

Hydration Mismatch — 가장 흔한 에러

function Greeting() {
  return <p>Today is {new Date().toLocaleDateString()}</p>;
}

server (KST timezone): "Today is 2026-05-26"
client (KST timezone): "Today is 2026-05-26"
                      → server 가 23:59 에 render 했고 client 가
                       자정 넘어서 render 하면 → "2026-05-27" 가 됨
                      → mismatch error

Error: Hydration failed because the initial UI does not match what
       was rendered on the server.

원인 type:
1. 시간 (Date.now, new Date)
2. random (Math.random, UUID)
3. browser-only API (window, localStorage, navigator)
4. 사용자별 (geolocation, cookies, IP-based)
5. 환경별 (server-only env vs client-only)

Mismatch 해결 패턴

방법 1 — 그 component 를 client-only:
  // Next.js
  const ClientOnlyTime = dynamic(() => import("./Time"), {ssr: false});

방법 2 — useEffect 안에서만 dynamic value 사용:
  function Greeting() {
    const [date, setDate] = useState<string | null>(null);
    useEffect(() => {
      setDate(new Date().toLocaleDateString());
    }, []);
    return <p>Today is {date ?? "loading"}</p>;
  }
  // server render = "loading"
  // client render = "loading" (match!) → 그 후 setDate → "2026-05-26"

방법 3 — suppressHydrationWarning (한 element 만):
  <p suppressHydrationWarning>
    {new Date().toLocaleDateString()}
  </p>
  // mismatch 경고 silent, client 값 우선
  // 이 element 의 자식들만 — recursive 아님

방법 4 — server / client 같은 값 보장:
  - 시간: server 에서 timestamp + client 에서 format
  - random: useId hook (React 18+)
  - locale: cookie 또는 URL 에서 일관

Progressive / Selective Hydration (React 18+)

전통 hydration (React 17 까지):
  page 의 모든 component 가 동시 hydrate
  → big page = 큰 main thread block
  → 그 동안 user interaction 무시

React 18 의 selective hydration:
  Suspense boundary 가 hydration unit
  - 처음 보이는 (above-fold) Suspense 부터 hydrate
  - 안 보이는 (below-fold) 는 idle 시간 또는 보일 때까지 deferred
  - 사용자가 below-fold 클릭하면 → 그 boundary 우선순위 ↑ → 먼저 hydrate

  <Suspense fallback={<Skel/>}>
    <Header />  ← 1st
  </Suspense>
  <Suspense fallback={<Skel/>}>
    <Sidebar />  ← 2nd (idle 시 hydrate)
  </Suspense>
  <Suspense fallback={<Skel/>}>
    <Comments />  ← 3rd (사용자 click 시 우선)
  </Suspense>

→ "Time to Interactive" 가 component 별로 조절. 큰 page 도 부분적으로 즉시 반응.

Island Architecture (Astro)

기본 가정 변경:
  React/SSR: page 전체가 React app — 모두 hydrate 필요
  Astro: page 가 정적 HTML — 일부 "island" 만 interactive

Astro 코드:
  ---
  // frontmatter (build-time only)
  const posts = await db.posts.findAll();
  ---
  <html>
    <body>
      <h1>{posts[0].title}</h1>   ← static HTML
      <p>{posts[0].body}</p>      ← static HTML
      <LikeButton client:load />  ← React/Vue island (hydrate)
      <CommentForm client:visible /> ← intersection 시 hydrate
    </body>
  </html>

→ static HTML 95% + interactive island 5% → JS 전송 매우 적음.
→ 블로그 / 마케팅 / 뉴스 사이트에 매우 적합.

Qwik — Resumability (hydration 없음)

Qwik (2022+) 의 다른 접근:
  - SSR HTML 에 모든 state + event handler reference 박음
  - hydration 단계 = 0 (즉시 interactive)
  - 사용자가 클릭 시점에만 그 handler 의 JS chunk lazy download

  HTML: <button on:click="/chunks/handleClick">Like</button>
                        ↑
                  click 시점에 fetch + 실행

장점:
  - O(1) startup — page 크기에 무관
  - 거대 application 도 instant interactive

단점:
  - mental model 학습 곡선
  - 각 handler 가 별도 chunk → 너무 작은 chunk 의 round-trip
  - ecosystem 작음

→ "hydration cost" 의 근본 해결책. React 와 다른 길.

흔한 함정

  • component 안 if (typeof window) 분기 — server / client 다른 결과 → mismatch. useEffect 안에서.
  • useState 의 initial 값에 dynamic 사용 — new Date() 등이 server / client 다름. lazy init + useEffect.
  • localStorage 직접 접근 — server 에 없음. useEffect 또는 useSyncExternalStore.
  • 큰 page 전체를 RSC 없이 hydrate — TTI 느림. Suspense boundary 또는 island 활용.
  • suppressHydrationWarning 남용 — 진짜 mismatch 숨기면 production 에서 silent UI corruption.

마무리

Hydration 은 SSR 의 cost — server HTML 위에 React 부착하는 단계. 시간 / random / browser-only API 가 mismatch 의 주범. modern React 18 의 selective hydration + Astro island + Qwik resumability 가 그 cost 를 줄이는 다양한 접근.

실용 — Next.js / Remix 의 default 활용 + dynamic value 는 useEffect 안 + 큰 sub-tree 는 Suspense boundary 로 분리. mostly-static content 면 Astro island 검토.

가이드 목록으로