Afaik

Hydration

Hydration이란 무엇이고 왜 필요한가요?

Hydration은 서버에서 렌더링된 정적 HTML에 JavaScript 기능을 결합하여 인터랙티브한 웹 애플리케이션으로 만드는 과정입니다. 쉽게 말해, "생명을 불어넣는" 과정이라고 할 수 있습니다.

Hydration의 핵심 개념

  1. 서버에서: HTML을 생성하여 브라우저에 전달
  2. 클라이언트에서: JavaScript가 로드되어 이벤트 리스너를 붙이고 상호작용 가능하게 만듦
  3. 결과: 빠른 초기 로딩 + 풍부한 사용자 상호작용

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은 다음과 같은 이유로 필요합니다:

  1. 빠른 초기 로딩: 사용자가 콘텐츠를 즉시 볼 수 있음
  2. SEO 최적화: 검색 엔진이 완전한 HTML을 크롤링 가능
  3. 점진적 향상: JavaScript 없이도 기본 기능 제공
  4. 사용자 경험: 콘텐츠 표시 → 인터랙션 활성화 순서로 단계적 로딩

신입 개발자가 기억해야 할 핵심:

  • 서버에서 HTML 생성 → 브라우저 전송 → JavaScript로 생명 불어넣기
  • 서버와 클라이언트 렌더링 결과가 일치해야 함 (Hydration Mismatch 방지)
  • 필요한 곳에만 인터랙션을 추가하여 성능 최적화
Edit on GitHub

Last updated on