Hydration
Hydration이란 무엇이고 왜 필요한가요?
Hydration은 서버에서 렌더링된 정적 HTML에 JavaScript 기능을 결합하여 인터랙티브한 웹 애플리케이션으로 만드는 과정입니다. 쉽게 말해, "생명을 불어넣는" 과정이라고 할 수 있습니다.
Hydration의 핵심 개념
- 서버에서: HTML을 생성하여 브라우저에 전달
- 클라이언트에서: JavaScript가 로드되어 이벤트 리스너를 붙이고 상호작용 가능하게 만듦
- 결과: 빠른 초기 로딩 + 풍부한 사용자 상호작용
Hydration이 없다면 어떻게 될까요?
<!-- 서버에서 렌더링된 정적 HTML (Hydration 전) -->
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div>
<h1>안녕하세요!</h1>
<button>클릭하세요</button>
<input type="text" value="안녕하세요" />
</div>
</body>
</html>위의 HTML만 있다면:
- ✅ 사용자는 콘텐츠를 볼 수 있음
- ❌ 버튼을 클릭해도 아무것도 일어나지 않음
- ❌ 입력 필드에 타이핑해도 반응하지 않음
- ❌ 모든 인터랙션이 작동하지 않음
Hydration 과정 상세 설명
1단계: 서버 사이드 렌더링 (SSR)
// 서버에서 실행되는 컴포넌트
function WelcomeComponent({ userName }: { userName: string }) {
return (
<div>
<h1>안녕하세요, {userName}님!</h1>
<button onClick={() => alert("환영합니다!")}>환영 메시지 보기</button>
<Counter />
</div>
);
}
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>카운터: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}서버에서 생성되는 HTML:
<div>
<h1>안녕하세요, 김철수님!</h1>
<button>환영 메시지 보기</button>
<div>
<p>카운터: 0</p>
<button>증가</button>
</div>
</div>2단계: 클라이언트에서 JavaScript 로드
// 브라우저에서 실행되는 JavaScript
import React from "react";
import { hydrateRoot } from "react-dom/client";
// 서버와 동일한 컴포넌트를 클라이언트에서 다시 렌더링
const container = document.getElementById("root");
hydrateRoot(container, <WelcomeComponent userName="김철수" />);3단계: 이벤트 리스너 연결 (Hydration 완료)
// Hydration이 완료되면:
// 1. onClick 이벤트 핸들러가 버튼에 연결됨
// 2. useState가 활성화되어 상태 관리 시작
// 3. 모든 React 기능들이 작동 시작
function Counter() {
const [count, setCount] = useState(0); // 이제 상태가 실제로 관리됨
return (
<div>
<p>카운터: {count}</p>
<button onClick={() => setCount(count + 1)}>
{" "}
{/* 클릭 이벤트 작동 */}
증가
</button>
</div>
);
}Next.js에서의 Hydration
App Router에서의 Hydration
// app/page.tsx (서버 컴포넌트)
export default async function HomePage() {
// 서버에서 데이터를 미리 가져옴
const posts = await fetch("https://api.example.com/posts").then((res) =>
res.json(),
);
return (
<div>
<h1>블로그</h1>
<PostList posts={posts} />
{/* 클라이언트 컴포넌트는 Hydration이 필요 */}
<InteractiveButton />
</div>
);
}
// 클라이언트 컴포넌트 (Hydration이 필요)
("use client");
import { useState } from "react";
function InteractiveButton() {
const [clicked, setClicked] = useState(false);
return (
<button
onClick={() => setClicked(!clicked)}
className={clicked ? "bg-green-500" : "bg-blue-500"}
>
{clicked ? "클릭됨!" : "클릭하세요"}
</button>
);
}Hydration이 필요한 이유
Hydration의 장점
1. SEO 최적화
- 검색 엔진이 완전한 HTML 콘텐츠를 즉시 크롤링 가능
- JavaScript 로딩을 기다릴 필요 없음
2. 빠른 초기 페이지 로딩
- 사용자가 콘텐츠를 즉시 볼 수 있음 (First Contentful Paint 개선)
- JavaScript 다운로드 완료를 기다리지 않음
3. 점진적 향상 (Progressive Enhancement)
- JavaScript가 비활성화되어도 기본 콘텐츠는 표시됨
- 네트워크가 느려도 HTML은 먼저 로드됨
실제 사용자 경험 시나리오
// 사용자 경험 타임라인
function BlogPost({ post }: { post: Post }) {
const [liked, setLiked] = useState(false);
const [comments, setComments] = useState<string[]>([]);
return (
<article>
{/* 0초: 사용자가 즉시 볼 수 있는 콘텐츠 (서버 렌더링) */}
<h1>{post.title}</h1>
<p>{post.content}</p>
<p>작성일: {post.date}</p>
{/* 1-2초 후: JavaScript 로드 완료, Hydration 시작 */}
{/* 이제 버튼들이 실제로 동작함 */}
<button
onClick={() => setLiked(!liked)}
className={liked ? "text-red-500" : "text-gray-500"}
>
{liked ? "♥" : "♡"} 좋아요
</button>
<CommentSection
comments={comments}
onAddComment={(comment) => setComments([...comments, comment])}
/>
</article>
);
}타임라인:
- 0초: HTML 콘텐츠 표시 (제목, 내용, 날짜)
- 1-2초: JavaScript 로드 및 Hydration 완료
- 2초+: 모든 인터랙션 기능 활성화 (좋아요, 댓글 추가 등)
Hydration 문제점과 해결책
Hydration 관련 주의사항
1. Hydration Mismatch
- 서버와 클라이언트에서 렌더링 결과가 다를 때 발생
- 날짜, 랜덤 값, 조건부 렌더링에서 자주 발생
2. JavaScript 번들 크기
- Hydration을 위해 모든 컴포넌트의 JavaScript가 필요
- 큰 번들 크기는 Hydration 완료 시간을 지연시킴
Hydration Mismatch 해결 예시
// ❌ 문제가 있는 코드
function ProblemComponent() {
return (
<div>
<p>현재 시각: {new Date().toLocaleString()}</p>
{/* 서버와 클라이언트에서 다른 시간이 렌더링됨 */}
</div>
);
}
// ✅ 올바른 해결책
function FixedComponent() {
const [currentTime, setCurrentTime] = useState<string>("");
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
setCurrentTime(new Date().toLocaleString());
}, []);
if (!mounted) {
// 서버 렌더링과 동일한 상태 유지
return (
<div>
<p>현재 시각: 로딩 중...</p>
</div>
);
}
return (
<div>
<p>현재 시각: {currentTime}</p>
</div>
);
}Next.js에서 Hydration 최적화
1. Selective Hydration
// 필요한 부분만 클라이언트 컴포넌트로 분리
export default function OptimizedPage() {
return (
<div>
{/* 서버 컴포넌트: Hydration 불필요 */}
<header>
<h1>정적 제목</h1>
<nav>정적 네비게이션</nav>
</header>
<main>
<StaticContent />
{/* 인터랙션이 필요한 부분만 클라이언트 컴포넌트 */}
<InteractiveWidget />
<CommentForm />
</main>
<footer>정적 푸터</footer>
</div>
);
}2. 지연 로딩으로 Hydration 최적화
import dynamic from "next/dynamic";
// 컴포넌트를 동적으로 로드하여 초기 번들 크기 감소
const HeavyInteractiveComponent = dynamic(
() => import("./HeavyInteractiveComponent"),
{
loading: () => <div>인터랙티브 컴포넌트 로딩 중...</div>,
ssr: false, // 서버 렌더링 건너뛰기
},
);
export default function Page() {
return (
<div>
<h1>메인 콘텐츠</h1>
<p>즉시 표시되는 내용</p>
{/* 필요할 때만 로드되는 무거운 컴포넌트 */}
<HeavyInteractiveComponent />
</div>
);
}디버깅 팁
// Hydration 상태를 확인하는 커스텀 훅
function useHydration() {
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
setHydrated(true);
}, []);
return hydrated;
}
// 사용 예시
function MyComponent() {
const hydrated = useHydration();
if (!hydrated) {
// 서버 렌더링 시와 동일한 UI
return <div>로딩 중...</div>;
}
// Hydration 완료 후의 인터랙티브 UI
return (
<div>
<InteractiveButton />
<DynamicContent />
</div>
);
}핵심 정리
Hydration은 다음과 같은 이유로 필요합니다:
- 빠른 초기 로딩: 사용자가 콘텐츠를 즉시 볼 수 있음
- SEO 최적화: 검색 엔진이 완전한 HTML을 크롤링 가능
- 점진적 향상: JavaScript 없이도 기본 기능 제공
- 사용자 경험: 콘텐츠 표시 → 인터랙션 활성화 순서로 단계적 로딩
신입 개발자가 기억해야 할 핵심:
- 서버에서 HTML 생성 → 브라우저 전송 → JavaScript로 생명 불어넣기
- 서버와 클라이언트 렌더링 결과가 일치해야 함 (Hydration Mismatch 방지)
- 필요한 곳에만 인터랙션을 추가하여 성능 최적화
Edit on GitHub
Last updated on