Hydration์ ์๋ฒ์์ ๋ ๋๋ง๋ HTML์ ํด๋ผ์ด์ธํธ ์ธก์์ ๋ค์ ๋ ๋๋งํ์ฌ ์ธํฐ๋ํฐ๋ธํ๊ฒ ๋ง๋๋ ๊ณผ์ ์ ์๋ฏธํฉ๋๋ค.
Next.js์ ๊ฐ์ ํ๋ ์์ํฌ์์๋ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR)์ ํตํด HTML์ ๋ฏธ๋ฆฌ ์์ฑํ์ฌ ๋น ๋ฅธ ์ด๊ธฐ ๋ก๋ฉ ์๋๋ฅผ ์ ๊ณตํ๊ณ , SEO์๋ ์ ๋ฆฌํ ํ๊ฒฝ์ ๋ง๋ญ๋๋ค.
์ดํ ํด๋ผ์ด์ธํธ ์ธก์์๋ Hydration์ ์ํํ์ฌ React ์ปดํฌ๋ํธ๋ฅผ ์ด๊ธฐํํ๊ณ , ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํ์ฌ ๋์ ์ธ ์ธํฐ๋์ ์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค.Hydration์ด ํ์ํ ์ด์ ๋ ์๋ฒ์์ ๋ ๋๋ง๋ HTML๋ง์ผ๋ก๋ ์ฌ์ฉ์ ์ธํฐ๋ ์ ์ ์ฒ๋ฆฌํ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์๋ฅผ ๋ค์ด, ์๋ฒ์์ ์์ฑ๋ ๋ฒํผ์ด ์๋ค๊ณ ํด๋ Hydration์ด ์๋ฃ๋๊ธฐ ์ ๊น์ง๋ ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
React๋ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์๋ฒ์์ ๋ ๋๋ง๋ ์์์ ํด๋ผ์ด์ธํธ์์ ๋ ๋๋ง๋ ์์๋ฅผ ๋น๊ต(React Reconciliation)ํ ํ, ํ์ํ ๋ถ๋ถ์ ์ ๋ฐ์ดํธํ์ฌ UI๋ฅผ ํ์ฑํํฉ๋๋ค.Hydration ๊ณผ์ ์์ ์ฃผ์ํด์ผ ํ ์ ์ ํด๋ผ์ด์ธํธ์ ์๋ฒ์์ ์์ฑ๋ UI๊ฐ ๋์ผํด์ผ ํ๋ค๋ ์ ์ ๋๋ค.
๊ทธ๋ ์ง ์์ผ๋ฉด React๊ฐ Hydration failed ์ค๋ฅ๋ฅผ ๋ฐ์์ํค๊ฑฐ๋, ๋ถํ์ํ ์ ์ฒด ๋ฆฌ๋ ๋๋ง์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํดuseEffect๋ฅผ ํ์ฉํ์ฌ ํด๋ผ์ด์ธํธ์์๋ง ์คํ๋ ๋ก์ง์ ๋ถ๋ฆฌํ๊ฑฐ๋,suppressHydrationWarning์ ์ฌ์ฉํ ์ ์์ต๋๋ค.๊ฒฐ๋ก ์ ์ผ๋ก, Hydration์ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง์ ์ด์ ์ ์ ์งํ๋ฉด์๋ ํด๋ผ์ด์ธํธ์์ ๋์ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ํต์ฌ์ ์ธ ๊ณผ์ ์ด๋ฉฐ, Next.js์์๋ ์ด๋ฅผ ์ต์ ํํ๊ธฐ ์ํด
useEffect,useHydration,next/dynamic๋ฑ์ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์์ต๋๋ค.
๐ก ์ฆ, SSR/SSG/ISR๋ ๋ณด์ฌ์ฃผ๋ ๋จ๊ณ, Hydration์ ์ด๋ฆฌ๋ ๋จ๊ณ๋ผ๊ณ ์ดํดํ๋ฉด ํธํ๋ค.
Next.js App Router ๊ธฐ์ค์ผ๋ก, ์ฒซ ๋ก๋ ์ ๋ ๋๋ง ํ์ดํ๋ผ์ธ์ ๋จ๊ณ๋ณ๋ก ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
app/ ๋ฐ์ layout.tsx, page.tsx ๋ฑ์ ๊ธฐ๋ณธ์ ์ผ๋ก Server Component์ด๋ค.ํด๋ผ์ด์ธํธ(๋ธ๋ผ์ฐ์ )์์ ํ๋ ์ผ (Hydration ํฌํจ)
๐ก ์ฌ๊ธฐ์ ํต์ฌ์, Hydration์ Server Component ์ ์ฒด๊ฐ ์๋๋ผ, Client Component ๋ถ๋ถ์ ๋์์ผ๋ก ํ๋ค.
- Server Component ๋ถ๋ถ์ HTML๋ง ์๊ณ , JS ์ธํฐ๋์ ์์ (Hydration ์์)
- Client Component๋ก ๊ฐ์ผ ๋ถ๋ถ๋ง Hydration ๋น์ฉ์ด ๋ ๋ค๊ณ ๋ณด๋ฉด ๋จ
app/ ์๋ ๋๋ถ๋ถ)"use client")onClick), ์ํ(useState), useEffect, ๋ธ๋ผ์ฐ์ API(window, localStorage) ๋ฑ์ด ํ์ํ ๊ฒฝ์ฐ๐ก ์ฆ, App Router์์๋ "Hydration ๋น์ฉ = Client Component์ ์๋งํผ"์ด๋ผ๊ณ ์ดํดํด๋ ๋๋ค.
๐ก ๊ทธ๋์ Next.js ๋ฌธ์์์๋ ๊ฐ๋ฅํ Server Component๋ฅผ ๋ง์ด ์ฌ์ฉํด JS ๋ฒ๋ค์ ์ค์ด๊ณ , FCP๋ฅผ ๊ฐ์ ํ๋ผ๊ณ ๊ถ์ฅํ๋ค.
๊ฐ๋จํ ์์
app/posts/[id]/page.tsx: Server Component
import LikeButton from "./LikeButton";
import { getPost } from "@/lib/data";
export default async function Page({ params }: { params: { id: string } }) {
const post = await getPost(params.id);
return (
<main>
<h1>{post.title}</h1>
{/* ์ฌ๊ธฐ๊น์ง๋ Server Component โ HTML๋ง ์์ฑ */}
<LikeButton initialLikes={post.likes} />
</main>
);
}
app/posts/[id]/LikeButton.tsx: Client Component
"use client";
import { useState } from "react";
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
return (
<button onClick={() => setLikes((v) => v + 1)}>
์ข์์ {likes}
</button>
);
}
- ์๋ฒ์์๋
LikeButton๊น์ง ํฌํจ๋ HTML์ ํ๋ฆฌ๋ ๋ํ๋ค.- ํด๋ผ์ด์ธํธ์์๋
LikeButton์ ๋ํ JS๊ฐ ๋ค์ด๋ก๋๋ ํ, ์ด ๋ฒํผ DOM์onClick๊ณผ state๋ฅผ ์ฐ๊ฒฐํ๋ Hydration์ด ์ํ๋๋ค.
์์ (React 17๊น์ง)์ SSR์ ๊ฑฐ์ "์ฌ-์ค์ด-๋ซ์ฑ(all-or-nothing) Hydration"์ด์๋ค.
loading.js ํ์ผ์ ์ฐ๋ฉด, Suspense๋ฅผ ํตํด ์ด ์คํธ๋ฆฌ๋ฐ ๊ฒฝํ์ ๋ ์ฝ๊ฒ ๊ตฌ์ฑํ ์ ์๋ค.๐ก Next.js App Router์์๋ Streaming SSR + Selective Hydration + Server Components๋ฅผ ์กฐํฉํด์ "๋นจ๋ฆฌ ๋ณด์ด๊ณ , ํ์ํ ๊ณณ๋ง ๋จผ์ ๋ฐ์ํ๋" ๋ ๋๋ง/ํ์ด๋๋ ์ด์ ์ ๋ต์ ๊ตฌํํ๊ณ ์๋ค.
Hydration์์ ๊ฐ์ฅ ์์ฃผ ๋ณด๋ ๊ฒ ๋ฐ๋ก Hydration mismatch ๊ฒฝ๊ณ /์๋ฌ์ด๋ค.
<span>1</span>์ ๋ณด๋๋๋ฐ, ํด๋ผ์ด์ธํธ์์๋ <span>2</span>์ด๋ผ๊ณ ์๊ฐํ๋ ์ํฉwindow, document, localStorage, navigator ๋ฑMath.random(), Date.now(), new Date() ๋ฑ์ ๋ฐ๋ก JSX์ ์ฌ์ฉํ๋ฉด, ์๋ฒ ๋ ๋๋ง๊ณผ ํด๋ผ์ด์ธํธ ๋ ๋๋ง ๊ฒฐ๊ณผ๊ฐ ๊ฑฐ์ ํญ์ ๋ค๋ฅด๋ค.localStorage์์๋ง ์ฝ์ด์ ๊ฒฐ์ ํ๋ UINext.js์์์ ๋ํ์ ์ธ ํด๊ฒฐ ํจํด
๋ธ๋ผ์ฐ์ ์ ์ฉ ๋ก์ง์ Client Component ์์์๋ง
"use client";
import { useEffect, useState } from "react";
export default function ClientOnly() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
// ์๋ฒ์ ํด๋ผ์ด์ธํธ๊ฐ ๋์ผํ๊ฒ ๋ ๋๋๋๋ก
// ์ด๊ธฐ์๋ ์๋ฌด๊ฒ๋ ์ ๋ณด์ฌ์ฃผ๊ฑฐ๋ skeleton๋ง ๋ณด์ฌ์ค
return null;
}
return <div>๋ธ๋ผ์ฐ์ ์ ์ฉ ์ ๋ณด: {window.innerWidth}</div>;
}
"use client" ํ์ผ ์์ผ๋ก ๋ถ๋ฆฌํ๊ณ ,useEffect, useState๋ก ๋ง์ดํธ ์ดํ์ ๋ธ๋ผ์ฐ์ ๊ฐ์ ์ฝ๋ ๋ฐฉ์์ ์ฌ์ฉํ๋ค.์๋ฒ/ํด๋ผ์ด์ธํธ ๋ ๋ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ๊ฒ ์ ์ง
useEffect ์ดํ์ ๋ฐ์ํ๋ค. (์: ์ค์ ์๊ฐ ์
๋ฐ์ดํธ)์ ๋ง ์ด์ฉ ์ ์์ ๋ suppressHydrationWarning
<div suppressHydrationWarning>
{isClient ? "ํด๋ผ์ด์ธํธ ๊ฐ" : "์๋ฒ ๊ฐ"}
</div>
Client-only ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋์ import + ssr: false
dynamic(() => import('./Chart'), { ssr: false }) ๋ฐฉ์์ผ๋ก ๋ ๋๋ง์ ํด๋ผ์ด์ธํธ์์๋ง ํ๊ฒ ํด์ mismatch๋ฅผ ์ ๊ฑฐํ๋ค.window, document, localStorage ๋ฑ์"use client" ์ปดํฌ๋ํธ + useEffect ์์์๋ง ์ฌ์ฉํ๋ค.ssr: false๋ก ์์ ํ ํด๋ผ์ด์ธํธ๋ฅผ ๋ ๋๋งํ๋ค.IntersectionObserver๋ "์คํฌ๋กค ์์ ์ ๋ก๋ฉ"๊ฐ์ ํจํด์ ์ฌ์ฉํ๋ค.suppressHydrationWarning์ ๋ง์ง๋ง ์๋จ์ผ๋ก๋ง ์ฌ์ฉํ๋ค.