Next.js: 서버 컴포넌트 렌더링 프로세스

작성일: 2025년 12월 15일 AM 11:43(마지막 수정: 2025년 12월 15일 PM 12:08)
조회수: 166

이전 글

React 렌더링 프로세스 알아보기

이제 Next.js 렌더링 프로세스를 알아볼 시간..!


SCCCtree.png

1. Next.js가 제공하는 3가지 서버 렌더링 전략 (Server Components와 함께)

**서버 컴포넌트(Server Component)**는 '서버 환경에서만 실행되는 컴포넌트'를 의미합니다.

이는 기존 React에서 사용되던 **클라이언트 컴포넌트(Client Component)**와는 구별되는 개념이며, 서버에서 UI 렌더링 및 선택적 캐싱 기능을 제공합니다.

서버 컴포넌트의 렌더링 방식을 자세히 살펴보기 전에, Next.js가 기본적으로 제공하는 3가지 서버 렌더링 전략을 먼저 알아보겠습니다.

전략 1: 정적 렌더링 (Static Rendering) - 기본값

Next.js의 서버 렌더링 전략 중 기본값입니다.

경로(Path)는 빌드 타임 또는 캐시가 무효화되어 데이터 재검증이 필요할 때 백그라운드에서 렌더링됩니다. 그 결과는 캐시되며, Content Delivery Network (CDN)에 배포될 수 있습니다.

CDN (Content Delivery Network): 분산된 서버 그룹으로 데이터 복사본을 저장하여, 최종 사용자와 가장 가까운 서버를 기준으로 데이터 요청을 처리할 수 있도록 합니다.

이 전략은 사용자에게 개인화되지 않은 데이터를 제공하며, 빌드 타임에 정적 경로를 알 수 있는 경우에 유용합니다.

예시: 블로그 게시물, 제품 상세 페이지

전략 2: 동적 렌더링 (Dynamic Rendering)

동적 렌더링은 경로가 요청 시 '각 사용자에게 맞게' 렌더링되는 전략입니다.

이는 정적 렌더링과 반대로, 사용자에게 개인화된 데이터가 있거나, 요청 시에만 알 수 있는 정보(쿠키 또는 URL의 검색 매개변수)가 있는 경로에 유용합니다.

참고: 모든 페이지가 완전히 정적이거나 동적일 필요는 없습니다. Next.js는 렌더링 과정에서 '동적 함수 호출' 또는 '캐시되지 않은 데이터 요청'이 발견되면, 전체 경로를 동적 렌더링으로 전환합니다. Next.js는 상황에 맞는 적절한 렌더링 전략(정적/동적)을 자동으로 선택하지만, 개발자는 캐시/재검증/UI 일부 스트리밍의 타이밍을 선택할 수 있습니다.

전략 3: 스트리밍 (Streaming)

이 전략은 아래 **'장점 7: 스트리밍'**에서 자세히 다루겠습니다.


2. 서버 컴포넌트 사용의 장점

서버 컴포넌트는 기존 클라이언트 컴포넌트와 비교하여 여러 가지 이점을 제공합니다.

장점 1: 데이터 페칭 성능 향상

데이터 소스에 더 가까운 서버에서 데이터를 페칭(Fetching) 할 수 있어, 데이터를 가져오는 시간과 요청 횟수를 줄일 수 있습니다.

장점 2: 보안 강화

민감한 데이터 및 로직(예: 토큰, API 키)을 클라이언트 환경에서 분리하여 서버에 유지함으로써 보안을 강화합니다.

장점 3: 강력한 캐싱 기능

서버에서 렌더링한 결과를 캐시하여, 이후 요청 및 사용자 간에 재사용할 수 있습니다. 이는 렌더링 및 데이터 페칭 작업의 양을 줄여줍니다.

Q: 클라이언트 캐싱과 서버 컴포넌트 캐싱의 차이점은 무엇인가요?

클라이언트 캐싱은 '개인 사용자'를 위한 것이지만, 서버 컴포넌트의 캐싱은 '전체 사용자'를 위한 공유 가능한 캐싱이라는 점이 가장 큰 차이점입니다.

Next.js 공식 문서에서 말하는 서버 컴포넌트 캐싱의 강력함을 3가지 핵심 포인트로 나누어 살펴보겠습니다.

1) '나만 빠름' vs. '모두가 빠름' (공유 가능성)

클라이언트 캐싱(React Query, SWR, 브라우저 캐시 등)은 해당 사용자의 브라우저에 저장됩니다.

  • 클라이언트 캐싱: 사용자 A가 접속하여 데이터를 가져오면 캐시가 되지만, 사용자 B가 접속하면 다시 서버에서 데이터를 가져와야 합니다.
  • 서버 컴포넌트 캐싱: 서버가 페이지를 렌더링하고 그 결과를 서버(또는 CDN)에 캐시해 둡니다. 사용자 A가 접속 후 캐시가 생성되면, 사용자 B는 렌더링이나 DB 조회를 다시 하지 않고 캐시된 결과를 즉시 받습니다.

핵심: 서버 캐싱은 한 번의 렌더링 결과를 수많은 사용자에게 재사용할 수 있습니다.

2) '재료 배달' vs. '완성품 배달' (캐싱의 대상)

클라이언트 캐싱과 서버 캐싱은 저장하는 **내용물(대상)**이 다릅니다.

비교 항목클라이언트 캐싱 (Client Component)서버 컴포넌트 캐싱 (Server Component)
저장 대상데이터 (JSON)렌더링 결과 (RSC Payload / HTML)
브라우저 역할JSON 데이터를 받아 컴포넌트 렌더링 및 화면 구성 (계산 수행)이미 완성된 UI 결과를 받아 화면에 표시만 함
부하 위치사용자의 기기 (스마트폰, PC)서버 (미리 한 번만 수행)

서버 캐싱은 데이터뿐만 아니라 '데이터를 HTML로 변환하는 연산 비용'까지 절약하여 저장합니다. 따라서 사용자의 기기 성능과 관계없이 화면이 매우 빠르게 로드됩니다.

3) 백엔드/DB 비용 절감 (비용 효율성)

서버 컴포넌트 캐싱이 적용되면, Next.js 서버는 이미 캐시된 결과가 있는지 확인하고, 있다면 데이터베이스(DB) 접근 없이 결과를 즉시 반환할 수 있습니다.

이로 인해 데이터베이스의 부하가 획기적으로 줄어들고, 클라우드 비용(API 호출 비용 등)을 크게 절감할 수 있습니다. 이것이 문서에서 말하는 비용을 절감하는 실질적인 의미입니다.

장점 4: 클라이언트 측 JavaScript (JS) 용량 감소

예를 들어, 비-인터랙티브(Non-interactive) 한 부분을 서버 컴포넌트로 옮기면 클라이언트 측에서 다운로드하고 파싱/실행해야 하는 JavaScript 용량을 줄일 수 있습니다.

이는 인터넷 속도가 느리거나 성능이 낮은 장치로 접근하는 사용자에게도 유리합니다.

장점 5: 초기 페이지 로드 및 FCP (First Contentful Paint) 개선

서버에서 HTML을 생성하여 전송하므로, 사용자가 페이지를 즉시 볼 수 있습니다.

클라이언트 컴포넌트처럼, 페이지를 렌더링하는 데 필요한 JS를 다운로드하고 파싱/실행할 필요가 없습니다.

장점 6: 검색 엔진 최적화 (SEO) 및 소셜 공유 개선

이미 렌더링이 완료된 HTML은 검색 엔진 봇이 페이지를 인덱싱하고 소셜 카드 미리보기를 생성하는 데 유리합니다.

장점 7: 스트리밍 (Streaming)

스트리밍은 Next.js App Router에 기본적으로 내장된 기능입니다.

loading.js 파일 및 React Suspense를 사용하여 경로 세그먼트의 스트리밍을 시작할 수 있습니다.

서버 컴포넌트의 스트리밍이란 무엇일까요?

서버에서 렌더링된 페이지의 HTML을 한 번에 보내지 않고, 데이터 준비가 끝나는 대로 작은 조각(Chunk)으로 나누어 클라이언트에 점진적으로 전송하는 기술입니다.

기존 SSR 방식은 페이지를 구성하는 데 시간이 오래 걸리는 작업이 있으면, 다른 컴포넌트들도 함께 대기해야 하는 블로킹(Blocking) 문제가 있었습니다.

페이지 렌더링 작업 순서: 데이터 페칭 (API/DB 호출) -> HTML 생성 (렌더링) -> HTML, CSS, JS 클라이언트로 전송 -> 하이드레이션 (상호작용 가능하게 만들기)

Next.js는 이 **렌더링 작업(Rendering Work)**을 **청크(Chunk)**로 분할하여, 준비되는 대로 클라이언트에게 스트리밍하는 방식을 사용합니다.

이를 통해, 전체 페이지가 서버에서 렌더링 완료될 때까지 기다리지 않고도, 사용자가 페이지의 일부를 먼저 볼 수 있습니다.


3. 서버 컴포넌트 렌더링 과정 (Rendering Work)

서버 컴포넌트가 최종적으로 화면에 표시되기까지의 과정을 서버와 클라이언트 단계로 나누어 자세히 살펴보겠습니다.

서버 환경에서 Next.js는 React의 API를 활용하여 렌더링 과정을 조정합니다.

전체 과정 요약

  1. 서버: 렌더링 작업을 청크로 분할.
  2. 서버: 각 청크별로, React는 서버 컴포넌트를 RSC 페이로드(Payload) 데이터 형식으로 렌더링합니다.
  3. 서버: 각 청크별로, Next.js는 RSC 페이로드와 클라이언트 컴포넌트 JS 지침에 따라 HTML을 렌더링하여 클라이언트에 스트리밍합니다.
  4. 클라이언트: HTML을 사용하여 비-인터랙티브 미리보기를 즉시 표시합니다 (초기 페이지 로드 시).
  5. 클라이언트: RSC 페이로드를 사용하여 **컴포넌트 트리(Server/Client)**를 **재조정(Reconcile)**하고 DOM을 업데이트합니다.
  6. 클라이언트: JS 지침에 따라 클라이언트 컴포넌트를 **하이드레이션(Hydration)**하여 앱을 인터랙티브하게 만듭니다.

1. 서버 - 렌더링 작업(Rendering Work)을 청크(Chunk)로 분할

렌더링 작업은 다음 두 가지 기준에 따라 청크로 분할됩니다.

  • 개별 경로 세그먼트: App Router에서 URL 경로의 각 부분을 나타내는 폴더.
  • Suspense 경계(Boundaries): React Suspense 컴포넌트가 감싸고 있는 부분.

분할된 청크는 각각 두 단계를 거쳐 서버에서 렌더링됩니다.

2. 서버 - React는 서버 컴포넌트를 RSC 페이로드로 렌더링 (각 청크별)

**RSC 페이로드(Payload)**는 '렌더링된 React 컴포넌트 트리의 간결한 이진 표현'이며, 브라우저가 DOM을 업데이트하는 데 사용됩니다.

RSC 페이로드는 다음과 같은 핵심 정보를 포함합니다.

  • 서버 컴포넌트의 렌더링 결과.
  • 클라이언트 컴포넌트가 렌더링되어야 할 위치(Placeholder) 및 관련 JS 파일에 대한 참조.
  • 서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 모든 props.

이처럼 서버 컴포넌트는 React에 의해 렌더링 결과 및 여러 정보를 담은 페이로드 데이터 형식으로 변환됩니다.

3. 서버 - Next.js는 HTML을 렌더링 (각 청크별)

Next.js는 RSC 페이로드와 클라이언트 컴포넌트 JS 로드 및 하이드레이션 지침에 따라 HTML을 렌더링합니다. 이 HTML은 초기 페이지 로드 시 사용자에게 표시될 내용을 담고 있습니다.

예시 (지침):

  • 어떤 클라이언트 컴포넌트를 로드해야 하는지, 해당 컴포넌트의 JS 번들은 어디에 있는지 명시합니다.
  • 서버에서 미리 렌더링된 HTML과 클라이언트 JS를 연결하는 하이드레이션 작업의 기준을 제공합니다.

4. 클라이언트 - 비-인터랙티브 HTML 미리보기 즉시 표시

서버에서 최종적으로 렌더링되어 스트리밍된 HTML을 화면에 즉시 표시합니다. 이 HTML은 정적이며, 클라이언트 컴포넌트를 포함한 JavaScript 코드가 하이드레이션되기 전 상태이므로 비-인터랙티브합니다.

5. 클라이언트 - 컴포넌트 트리 재조정(Reconcile) 후 DOM 업데이트

RSC 페이로드에 포함된 정보(클라이언트 컴포넌트의 위치, props 등)를 사용하여 서버 컴포넌트와 클라이언트 컴포넌트로 구성된 React 컴포넌트 트리를 **재조정(Reconcile)**합니다.

이 재조정을 통해 변경된 사항이 DOM에 반영되어 화면이 업데이트됩니다.

6. 클라이언트 - 하이드레이션 (Hydration)

마지막으로, HTML을 인터랙티브하게 만드는 단계입니다. 이벤트 핸들러와 같은 JavaScript 코드가 DOM에 연결되어(Attach) 상호작용이 가능한 동적인 웹 페이지가 완성됩니다.


실습

어떤 서버 컴포넌트 기반 페이지에서, 다음과 같은 클라이언트 컴포넌트가 있다면,

HTML preview단계에서는 어떻게, 컴포넌트 트리 단계에서는 어떻게, 하이드레이션 단계에서는 최종적으로 어떻게 보여질 지 예측해보세요

jsx
"use client";

import { useState } from "react";

export default function ClientComponent() {
    const [value, setValue] = useState(0);

    return (
        <div>
            ClientComponent
            <button
                onClick={() => {
                    console.log("test string");
                    setValue((prev) => prev + 1);
                }}
            >
                Click me
            </button>
            <p>Value: {value}</p>
        </div>
    );
}

참고 자료

0개의 댓글
💬

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!