Hydration과 서버 사이드 렌더링

Intro

프론트엔드에서 Next.js를 사용함으로써 얻을 수 있는 가장 큰 장점은 서버 사이드 렌더링의 활용이라 생각한다.

이를 위해 Hydration의 개념을 이해하고 Next.js가 브라우저를 렌더링하는 방식을 살펴보자.

(본 글은 Next.js 13 이상 App Router를 기준으로 작성되었습니다)

1. Hydration

hydration 단어 자체의 의미는 물을 주는 것이다.

다시 말해 서버에서 렌더링 된 정적 HTML에 상호작용을 가능하게 해주는 JavaScript 입히는 과정을 의미한다.

요약하자면 서버에서 생성된 HTML에 React가 상태를 부여해 상호작용성을 추가하는 과정이다.

이 과정에서 React는 HTML과 자신의 상태를 동기화하여 UI를 효율적으로 관리할 수 있게 된다.

1.1 Hydration의 동작 방식

  • 서버에서 React 컴포넌트를 HTML로 SSR 한다
  • 생성된 HTML을 클라이언트로 전송한다
  • 브라우저에서 서버에서 받은 HTML에 대응하는 React 컴포넌트의 JavaScript를 로딩하여 상호작용성을 부여한다
  • React가 HTML에 JavaScript를 연결하며 상태와 상호작용이 활성화된다

1.2 Hydration의 장점

이러한 Hydration의 장점은 곧 서버 사이드 렌더링의 장점이다.

  • FCP: 사용자는 JavaScript가 완전히 로드되기 전에 콘텐츠를 미리 볼 수 있다. 이때 HTML은 렌더링되어 있지만, JavaScript가 아직 연결되지 않은 상태이다
  • SEO: 서버에서 완성된 HTML이 전송되므로, 검색 엔진이 페이지 내용을 쉽게 파악할 수 있어 SEO가 유리하다

2. Next.js가 브라우저를 렌더링하는 방식

Next.js는 Server ComponentsClient Components로 나뉘어져 있어, 각 컴포넌트의 특성에 따라 서버나 클라이언트에서 렌더링할 수 있다. 이를 통해 페이지의 성능을 최적화할 수 있다.

2.1 Server Components

async function Posts() {
  const posts = await fetchPosts();
  return (
    <div>
      {posts.map((post) => (
        <Post key={post.id} {...post} />
      ))}
    </div>
  );
}

Next.js는 기본적으로 Server Components를 사용하며, 필요에 따라 Client Components를 사용할 수 있다.

Next.js Server Components의 특징은 크게 다음과 같다.

장점

  • 서버에서 실행되어 HTML로 변환된다
  • 클라이언트 번들 크기를 줄여 초기 로딩 속도가 빨라진다
  • 데이터베이스에 직접 접근이 가능하다
  • 민감한 정보를 클라이언트에 노출하지 않고 서버에서 안전하게 다룰 수 있다

제한사항

  • 브라우저 API 사용이 불가능하다 (window, document 등)
  • 이벤트 핸들러를 사용할 수 없다 (onClick 등)
  • React의 상태 관련 훅을 사용할 수 없다 (useState, useEffect 등)

2.2 Client Components

'use client'; // Client Component 선언

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Client Component는 최상단에 "use client"를 명시해줘야 한다.

Client Components의 특징은 다음과 같다.

  • 브라우저에서 실행되어 상호작용과 이벤트 처리 가능하다
  • useState, useEffect 등 브라우저 환경에서 필요한 훅을 사용할 수 있다
  • 브라우저 API를 자유롭게 사용할 수 있다

2.3 서버 렌더링 종류

Next.js에서 지원하는 서버 렌더링 방식에는 Static Rendering, Dynamic Rendering, Streaming Rendering 등이 있으며, 각 렌더링 방식은 특성과 활용 목적이 다르다.

Static Rendering (Default)

  • 빌드 시점에 생성되는 정적 콘텐츠로, 자주 변경되지 않는 콘텐츠에 적합하다
  • 생성된 HTML은 캐시될 수 있으며 CDN에서 제공되어 매우 빠른 초기 로딩 속도를 제공한다
  • generateStaticParams를 사용하여 동적 라우트의 정적 페이지를 생성할 수 있다
  • ISR(Incremental Static Regeneration) 을 통해 빌드 후에도 주기적으로 페이지를 재생성할 수 있어, 실시간 데이터를 부분적으로 반영할 수 있다
// 정적 경로 생성 예시
export async function generateStaticParams() {
  const posts = await fetchPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

Dynamic Rendering

  • 요청 시점에 생성되는 동적 콘텐츠로, 사용자별 맞춤 콘텐츠를 제공할 때 유용하다
  • App Router에서는 사용자의 쿠키, 현재 요청 헤더 또는 URL의 검색 매개변수와 같이 요청 시에만 알 수 있는 정보에 의존하는 동적 함수(cookies(), headers() 등) 사용 시 자동으로 Dynamic Rendering으로 전환된다
import { cookies } from 'next/headers';

export default async function Page() {
  const cookieStore = cookies();
  const theme = cookieStore.get('theme');
  return `현재 테마: ${theme}`;
}

Streaming Rendering

  • 컴포넌트 단위의 점진적 렌더링 방식으로, 초기 렌더링 속도를 더욱 높일 수 있다
  • Next.js의 loading.jsSuspense를 통해 로딩 상태를 관리하며, 중요한 콘텐츠를 우선 제공하는 방식이다
import { Suspense } from 'react';
import Loading from './loading';

export default function Page() {
  return (
    <div>
      <h1>즉시 로딩되는 콘텐츠</h1>
      <Suspense fallback={<Loading />}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

마무리

Next.js의 렌더링 방식은 Server Components와 Client Components의 장점을 모두 활용하여 최적화된 사용자 경험을 제공한다.

Hydration을 통해 서버에서 렌더링된 HTML에 상호작용성을 부여하고, 이를 통해 성능과 개발 경험 모두를 향상시킬 수 있다.

각 렌더링 방식과 컴포넌트의 특성을 이해하고 적절하게 사용함으로써, 웹 애플리케이션의 성능과 사용자 경험을 극대화할 수 있다.

Reference

Server Components Client Components Next.js의 렌더링 과정(Hydrate) 알아보기