프로필 로고
2026-04-07

Server Component

RSC Payload를 중심으로 Next.js의 렌더링 흐름을 설명하고, Static·Dynamic·Streaming 세 가지 서버 렌더링 전략과 데이터 캐시의 독립적 관리 방식을 정리한다.

  • Next.js
  • RSC
  • 캐싱
  • Streaming
  • 렌더링

배경

  1. SSR은 서버에서 HTML을 완성해 브라우저로 보내므로, 사용자는 JS 실행 전에도 콘텐츠를 볼 수 있다.
  2. 그러나, 서버는 데이터 페칭이 완전히 끝나야 HTML 생성을 시작할 수 있어 응답 지연이 발생한다.
  3. 또한, 페이지를 인터랙티브하게 만들려면 하이드레이션 과정이 필요한데, 이는 React가 브라우저에서 전체 컴포넌트 트리를 다시 구성하는 작업이다.
  4. 마지막으로, 하이드레이션은 트리 전체를 한 번에 처리하므로, 하나라도 느리면 페이지 전체의 인터랙션이 막힌다.
  5. 결정적으로 SSR도 CSR처럼 모든 컴포넌트의 JavaScript를 클라이언트에 전송해야 했다.
  6. 즉, 정적인 헤더나 텍스트 컴포넌트조차 하이드레이션을 위해 JS 번들에 포함되는 비효율이 있었다.
  7. 이 문제를 해결하기 위해 React 팀은 React Server Components(RSC)를 도입했다.
  8. RSC의 핵심 아이디어는 컴포넌트 단위로 서버/클라이언트 실행 환경을 분리하는 것이다.
  9. 인터랙션이 필요 없는 컴포넌트는 서버에서만 실행되고, 그 결과만 클라이언트로 전송된다.
  10. 따라서 Server Component는 클라이언트 JavaScript 번들에 포함되지 않아 번들 크기가 줄어드낟.
  11. 반면 SSR은 페이지 단위로만 서버 렌더링을 적용할 수 있었던 반면, RSC는 컴포넌트 단위로 제어가 가능하다.

Next.js의 렌더링 흐름과 RSC Payload

  1. Next.js에서 서버 컴포넌트는 별도 설정 없이 기본으로 활성화되어 있다.
  2. 서버에서 React는 서버 컴포넌트를 RSC Payload라는 특수한 바이너리 데이터 형식으로 변환한다.
  3. RSC Payload에는 세 가지 정보가 담겨 있다.
  4. 첫째, 서버 컴포넌트의 렌더링 결과물이다.
  5. 둘째, 클라이언트 컴포넌트가 삽입되어야 할 위치와 해당 JS 파일에 대한 참조이다.
  6. 셋째, 서버 컴포넌트에서 클라이언트 컴포넌트로 전달된 props 데이터이다.
  7. Next.js는 이 RSC Payload와 클라이언트 컴포넌트 JS 지침을 합쳐 최종 HTML을 생성한다.
  8. 클라이언트는 이 HTML을 즉시 화면에 표시하고(초기 로드), RSC Payload로 DOM을 동기화한다.
  9. 마지막으로 JS 지침으로 클라이언트 컴포넌트만 하이드레이션하여 페이지를 인터랙티브하게 만든다.
  10. 이 흐름 덕분에 서버 컴포넌트는 하이드레이션이 전혀 없어도 되고, 클라이언트 컴포넌트만 선택적으로 인터랙티브해진다.
  11. 서버 렌더링 전략은 Static, Dynamic, Streaming 세 가지로 구분된다.

Static Rendering

  1. Static Rendering은 빌드 타임에 경로를 미리 렌더링하는 방식이다.
  2. 렌더링 결과는 캐시되어 CDN에 배포되므로, 여러 사용자 간 렌더링 결과를 공유할 수 있다.
  3. 블로그 게시물이나 제품 페이지처럼 개인화가 필요 없고 내용이 정적인 경로에 적합하다.
  4. 데이터가 갱신되면 재검증(revalidation) 후 백그라운드에서 다시 렌더링된다.

Dynamic Rendering

  1. Dynamic Rendering은 요청이 들어올 때마다 각 사용자에 맞게 경로를 새로 렌더링한다.

  2. 쿠키, 요청 헤더, URL 검색 파라미터처럼 요청 시에만 알 수 있는 데이터가 필요한 경우에 사용한다.

  3. Next.js에서 아래 동적 API를 사용하면 해당 경로 전체가 자동으로 Dynamic Rendering으로 전환된다.

    cookies()        // 서버 컴포넌트에서 HTTP 요청의 쿠키를 읽을 수 있게 해주는 비동기 함수
    headers()        // 서버 컴포넌트에서 HTTP 요청의 헤더를 읽을 수 있게 해주는 비동기 함수
    searchParams     // URL 쿼리 파라미터
    unstable_noStore() // 캐시 명시적 비활성화
    connection()     // 렌더링 과정에서 사용자 요청을 기다려야 하는지 여부를 나타낼 수 있게 해주는 비동기 함수
  4. 중요한 점은 동적 경로 안에서도 일부 데이터는 캐시할 수 있다는 것이다.

  5. RSC Payload와 데이터 캐시가 별도로 관리되기 때문에 이것이 가능하다.

  6. 개발자가 Static/Dynamic을 직접 선택할 필요는 없고, Next.js가 사용된 API에 따라 자동으로 판단한다.

동적 경로 안에서도 일부 데이터는 캐시할 수 있다는 말은 무엇일까?

  1. 동적 경로란, 해당 페이지가 요청마다 새로 렌더링되는 경로를 말한다.

  2. 예를 들어 cookies()를 쓰는 순간, 그 페이지 전체는 동적 경로가 된다.

  3. "동적 경로 = 모든 데이터를 매 요청마다 새로 가져온다"고 착각하기 쉽다.

  4. 그러나 실제로는 동적 경로 안에서도 데이터마다 캐시 여부를 개별적으로 설정할 수 있다.

  5. 아래 예시를 보면, 같은 페이지 안에 캐시되는 데이터와 캐시되지 않는 데이터가 공존한다.

    export default async function Page() {
      // 이 데이터는 캐시됨 — 매 요청마다 DB를 치지 않음
      const products = await fetch('/api/products', { cache: 'force-cache' });
     
      // 이 데이터는 캐시 안 됨 — 매 요청마다 새로 가져옴
      const user = await fetch('/api/me', { cache: 'no-store' });
    }
  6. 즉, 동적 경로라는 것은 "렌더링 자체는 매 요청마다 한다"는 의미이지, "모든 데이터를 매번 새로 가져온다"는 의미가 아니다.

RSC Payload와 데이터 캐시가 별도로 관리된다는 말의 의미

  1. 이걸 이해하려면 Next.js가 두 가지를 따로 저장한다는 걸 알아야 한다.

  2. 하나는 RSC Payload 캐시이고, 다른 하나는 데이터 캐시이다.

  3. RSC Payload 캐시는 컴포넌트 트리의 렌더링 결과물을 저장한다.

  4. 데이터 캐시는 fetch 등으로 가져온 원본 데이터를 저장한다.

  5. 이 둘이 분리되어 있기 때문에, 렌더링은 매번 새로 해도 데이터는 캐시된 것을 꺼내 쓸 수 있다.

    [요청 발생]
    
    [렌더링 실행] ← 동적이므로 매번 실행됨
    
    [데이터 필요] → 데이터 캐시에 있으면 꺼내  (DB 안 침)
                   → 없으면 새로 fetch
  6. 즉 "동적 렌더링"과 "데이터를 매번 새로 가져오는 것"은 별개의 일이다.

  7. RSC Plaload와 데이터가 별도로 캐싱되기 때문에, 요청 시 모든 데이터를 가져올 때 성능에 미치는 영향에 대해 신경 쓸 필요 없이 동적 렌더링을 사용해도 된다는 의미이다.

Streaming

  1. 기존 SSR은 전체 데이터 페칭과 렌더링이 끝나야 HTML을 전송할 수 있어 느린 데이터가 전체를 차단했다.

  2. Streaming은 이 문제를 해결하기 위해 렌더링 결과를 청크 단위로 나눠 준비되는 즉시 클라이언트로 전송한다.

  3. 전체 페이지가 완성될 때까지 기다리지 않아도 사용자가 일부 UI를 먼저 볼 수 있다.

  4. 느린 데이터 페칭이 있는 일부 UI가 나머지 페이지 로딩을 막는 문제를 해결한다.

  5. Next.js App Router에는 Streaming이 기본으로 내장되어 있다.

  6. loading.js 파일이나 React Suspense를 통해 어떤 부분을 스트리밍할지 세부적으로 제어할 수 있다.

    import { Suspense } from 'react';
    import Reviews from './Reviews'; // 느린 데이터 페칭
     
    export default function ProductPage() {
      return (
        <div>
          <h1>제품 정보</h1> {/* 즉시 표시 */}
          <Suspense fallback={<p>리뷰 불러오는 중...</p>}>
            <Reviews /> {/* 준비되면 스트리밍 */}
          </Suspense>
        </div>
      );
    }
  7. 위 예시처럼 느린 리뷰 컴포넌트가 나머지 페이지 렌더링을 차단하지 않도록 분리할 수 있다.

렌더링 흐름

  1. 사용자가 특정 경로에 접속하면 Next.js 서버가 해당 페이지의 서버 컴포넌트 트리를 실행한다.
  2. 실행 결과는 RSC Payload라는 스트리밍 가능한 포맷으로 직렬화된다.
  3. 이 Payload와 함께 클라이언트 컴포넌트에 필요한 JS 번들만 선택적으로 브라우저로 전송된다.
  4. 브라우저는 RSC Payload를 해석해 DOM을 구성하고, 클라이언트 컴포넌트 부분만 hydration한다.
  5. 결과적으로 서버 컴포넌트만으로 구성된 페이지는 hydration 비용이 0에 가깝다.