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

작성일: 2025년 12월 15일 오전 11:43(마지막 수정: 2025년 12월 15일 오후 12:08)
조회수: 62

이전 글

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.jsRSC 페이로드클라이언트 컴포넌트 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개의 댓글
💬

아직 댓글이 없습니다

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