blog

RSC와 SSR의 관계: RSC Payload부터 'use client'와 Hydration까지

Front-end / Next.js#React#RSC#SSR#Hydration#App Router

개요

React Server Components(RSC)와 Server-Side Rendering(SSR)은 함께 사용할 수 있지만, 같은 개념은 아니다. RSC는 서버에서 컴포넌트를 실행하고 그 결과를 전달하는 모델이고, SSR은 서버에서 React UI를 HTML로 만드는 과정이다.

이 글은 Next.js App Router의 일반적인 동작을 기준으로 설명한다. React의 개념과 Next.js의 구현 방식을 구분하며, 코드와 흐름도는 이해를 돕기 위해 단순화한 예시다. 공식 문서는 2026년 10월 13일에 확인했다.

1. RSC와 SSR은 서로 다른 문제를 해결한다

Server Component는 브라우저에서 실행되지 않는다. 서버 환경에서 데이터나 파일을 읽고 UI를 계산하며, 그 컴포넌트 구현 코드를 브라우저에 보낼 필요가 없다. 이 실행은 요청 시점뿐 아니라 빌드 시점에도 가능하다. 따라서 RSC를 사용한다고 매 요청마다 서버가 실행되어야 하는 것은 아니다. React: Server Components

SSR은 React 트리를 HTML로 렌더링한다. RSC 없이도 React의 서버 렌더링 API로 HTML을 만들 수 있다. HTML을 받은 뒤 React 상호작용을 활성화하려면 클라이언트 코드와 hydration을 연결한다. 정적인 HTML만 제공하는 경우라면 SSR 자체가 hydration을 요구하는 것은 아니다. React: renderToPipeableStream

구분RSCSSR
중심 질문어떤 컴포넌트 코드를 서버에서 실행하고 결과를 전달할까?어떻게 서버에서 초기 HTML을 만들까?
대표 결과물Server Component 실행 결과를 담은 RSC PayloadHTML
브라우저에 필요한 코드Server Component 구현은 전달하지 않음상호작용을 위한 클라이언트 코드는 별도로 필요
서로의 필요 여부SSR과 조합 가능하나 SSR이 필수는 아님RSC 없이도 사용 가능

서버에서 만든 HTML을 모두 요청 시점 SSR이라고 부르면 안 된다

Next.js는 HTML을 빌드 시점에 만들거나(SSG), 빌드 이후 재생성하거나(ISR), 요청 시점에 만들 수 있다(SSR). 컴포넌트 종류와 HTML 생성 시점은 별개의 축이다. Client Component도 이 HTML 생성에 참여할 수 있다. 이후 설명에서 사용하는 '서버 사전 렌더링'은 요청 시점 SSR만을 뜻하지 않는다. Next.js: Server and Client Boundary

2. 하나의 페이지에서 Server Component와 Client Component를 조합하기

다음은 설명용 데이터와 카운터를 조합한 예제다. App Router의 기본 루트 레이아웃은 이미 있다고 가정한다.

// app/page.tsx
import Counter from './Counter';

async function getProfile() {
  // 예시 데이터. 실제 앱에서는 서버 데이터 조회로 대체한다.
  return { name: '민수', initialCount: 3 };
}

export default async function Page() {
  const profile = await getProfile();

  return (
    <main>
      <h1>{profile.name}의 페이지</h1>
      <Counter initialCount={profile.initialCount} />
    </main>
  );
}
// app/Counter.tsx
'use client';

import { useState } from 'react';

export default function Counter({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount);

  return (
    <button onClick={() => setCount((previous) => previous + 1)}>
      클릭 수: {count}
    </button>
  );
}

Page는 서버에서 데이터를 준비한다. Counter는 브라우저에서 상태와 클릭 이벤트를 처리한다. Client Component 경계는 import 앞의 'use client'로 선언한다. Next.js: use client

3. RSC Payload는 무엇을 담는가?

RSC Payload는 서버에서 계산한 UI를 React에 전달하는 직렬화된 표현이다. Server Component의 렌더링 결과, Client Component가 들어갈 위치와 JavaScript 모듈 참조, 서버에서 클라이언트로 전달하는 props를 담는다. HTML이나 컴포넌트 소스 코드와는 다르다. Next.js: Server and Client Components

앞의 예제를 개념적으로 표현하면 다음과 같다.

main
├─ h1: "민수의 페이지"
└─ Client Component 참조: Counter
   └─ props: { initialCount: 3 }

이 구조는 실제 전송 프로토콜을 재현한 것이 아니다. 실제 Payload를 일반적인 JSON 객체라고 생각하거나, 애플리케이션에서 직접 파싱해야 하는 API 응답이라고 이해하면 안 된다.

서버에서 클라이언트로 넘기는 props는 React가 지원하는 직렬화 가능한 값이어야 한다. 이 범위는 JSON보다 넓으며 Date, Map, Set 등도 지원한다. 일반적인 이벤트 콜백을 서버에서 만들어 그대로 넘길 수는 없다. Server Function 참조는 별도로 지원된다. React: Serializable types

4. 초기 로딩: HTML이 있는데 Payload는 왜 필요한가?

주소를 직접 열거나 새로고침하면, Next.js는 RSC Payload와 Client Component 코드를 사용해 초기 HTML을 만든다. 브라우저는 HTML로 화면을 표시하고, React는 Payload로 서버 결과와 클라이언트 부분을 하나의 트리로 연결한다. Client JavaScript는 hydration을 통해 상호작용을 활성화한다. Next.js: First load

[서버 환경]
Server Component 실행 → RSC Payload 생성
                           │
                           └─ Client Component와 함께 HTML 생성
                                              │
                           HTML + Payload + 필요한 JS 전달
                                              ↓
[브라우저]
HTML로 초기 화면 표시
    ↓
Payload로 서버 결과와 Client Component 트리 연결
    ↓
Client Component hydration → 상태와 이벤트 처리
전달물역할
HTML초기 화면을 표시할 마크업
RSC PayloadReact가 서버에서 계산한 UI를 트리에 반영할 정보
Client JavaScriptClient Component 실행과 상호작용

HTML에는 Counter의 모듈 참조나 React props 같은 정보가 모두 담겨 있지 않다. 화면 표시와 React 트리 연결에 서로 다른 정보가 필요한 것이다.

흐름도는 논리적인 관계를 보여준다. 실제 렌더링과 전송은 스트리밍으로 겹쳐 진행될 수 있으며, 모든 HTML을 완성한 뒤 다른 작업이 시작되는 고정 순서는 아니다. React: Streaming more content

5. 클라이언트 내비게이션: 새 HTML 문서 없이 UI 갱신하기

App Router의 <Link>로 페이지를 이동하는 경우를 보자.

import Link from 'next/link';

export default function Navigation() {
  return <Link href="/products">상품 목록</Link>;
}
Link를 통한 클라이언트 내비게이션
    ↓
사용 가능한 프리패치·캐시 결과가 있는가?
    ├─ 있음 → 해당 결과 사용
    └─ 부족함 → 필요한 서버 결과 요청
                    ↓
             RSC Payload 수신
    ↓
React가 서버 UI 결과와 Client Component를 조합
    ↓
변경된 UI 반영, 공유 레이아웃 재사용

클릭할 때마다 반드시 새 요청을 보내는 것은 아니다. Next.js는 이동 전에 경로 데이터를 미리 가져올 수 있다. 서버에서도 이미 생성한 결과를 재사용할 수 있으므로, 클릭마다 Server Component와 DB 조회가 새로 실행된다고 단정할 수 없다. 프리패치 범위와 캐시 동작은 경로와 설정에 따라 달라진다. Next.js: Linking and Navigating

일반적인 클라이언트 내비게이션에서는 새 페이지 전체 HTML 문서를 받아 교체하지 않는다. Server Component의 결과는 Payload로 반영하고, 새로 필요한 Client Component는 브라우저에서 렌더링한다. 이 경우 새 컴포넌트의 렌더링을 초기 HTML에 대한 hydration과 구분해야 한다. Next.js: Subsequent Navigations

공유 레이아웃은 재사용되지만, 이동한 페이지의 모든 상태가 보존된다는 뜻은 아니다. 다시 마운트되는 컴포넌트의 상태까지 보존한다고 가정하면 안 된다. 새로고침이나 문서 전체를 여는 이동은 초기 로딩 경로에 해당한다. Next.js: Client-side transitions

따라서 이 과정을 단순히 'RSC + CSR'이라고만 부르면 서버 계산이 사라진 것처럼 들릴 수 있다. 서버가 만든 UI 결과를 받아 브라우저에서 React 트리를 갱신한다고 설명하는 편이 정확하다.

6. 'use client'는 SSR을 끄는 옵션이 아니다

'use client'는 모듈 의존성 그래프의 서버·클라이언트 경계를 표시한다. 해당 모듈과 그 import 의존성은 클라이언트 코드에 포함된다. 컴포넌트뿐 아니라 유틸리티도 이 경계의 영향을 받는다. 반대로 지시어가 없는 파일이라도 클라이언트 모듈에서 import하면 클라이언트에서 사용될 수 있다. React: use client

Page.tsx                 서버 모듈
└─ Counter.tsx           'use client' 경계
   ├─ Button.tsx         클라이언트 의존성
   └─ formatCount.ts     클라이언트 의존성

JSX의 부모·자식 관계와 import 관계는 구분해야 한다. 서버 부모가 만든 UI를 Client Component의 children으로 전달해도, 그 UI를 만든 Server Component가 클라이언트 모듈로 바뀌지는 않는다. Next.js: Interleaving Server and Client Components

Client Component의 'Client'는 브라우저에서도 실행된다는 뜻이다. 초기 HTML 생성 과정에서는 서버에서도 렌더링될 수 있다. 서버에서 HTML을 만들었다고 Server Component가 되는 것은 아니다. Next.js: Server and Client Boundary

동작Server ComponentClient Component
서버 환경에서 컴포넌트 실행가능초기 HTML 생성 시 가능
브라우저에서 컴포넌트 실행하지 않음가능
구현 코드의 클라이언트 전달서버 전용 사용 부분은 제외필요
useState, useEffect, 클릭 이벤트사용 불가사용 가능
초기 HTML에 기여가능기본적으로 가능
초기 HTML에 대한 hydration컴포넌트 구현 자체는 대상이 아님상호작용을 위해 필요

Server Component가 브라우저에서 다시 실행되지 않는다고 해서 React가 그 결과를 다루지 않는 것은 아니다. React는 Payload로 받은 결과를 트리에 반영한다.

7. Hydration은 기존 HTML과 React를 연결하는 과정이다

Hydration은 이미 서버에서 생성한 HTML에 React를 연결하고 상호작용을 활성화한다. 단순히 HTML을 다시 만드는 과정으로 이해하면 부족하다. React의 hydrateRoot는 서버 HTML과 클라이언트의 첫 렌더링 결과가 일치할 것을 전제로 한다. Next.js 같은 프레임워크가 이 연결을 처리하므로 App Router의 페이지마다 직접 호출하지 않는다. React: hydrateRoot

'use client'가 있어도 window를 바로 읽으면 문제가 된다

'use client';

export default function Width() {
  // 잘못된 예시: 서버 렌더링에서는 window가 없다.
  return <p>화면 너비: {window.innerWidth}</p>;
}

Client Component도 서버에서 렌더링될 수 있다. 브라우저 API를 초기 렌더링 중 사용하는 대신 Effect나 이벤트 핸들러 등 브라우저에서 실행되는 위치에서 접근해야 한다. 다음 예시에서는 서버와 브라우저의 첫 렌더링이 모두 같은 대기 문구를 만든다.

'use client';

import { useEffect, useState } from 'react';

export default function Width() {
  const [width, setWidth] = useState<number | null>(null);

  useEffect(() => {
    setWidth(window.innerWidth);
  }, []);

  return <p>화면 너비: {width === null ? '확인 중' : `${width}px`}</p>;
}

이 예시는 마운트 후 너비를 한 번 읽는다. useEffect는 서버 렌더링 중 실행되지 않는다. 브라우저에서는 연결 후 실행되어 화면을 갱신한다. React: useEffect

typeof window !== 'undefined'로 오류만 피하더라도 서버와 브라우저의 첫 마크업이 달라지면 hydration 불일치가 발생할 수 있다. 날짜, 난수, 브라우저 저장소 값으로 첫 렌더링 결과가 달라지는 경우도 주의해야 한다. React: Hydrating server-rendered HTML

8. 특정 컴포넌트의 서버 사전 렌더링을 제외하려면?

Next.js에서는 next/dynamic의 { ssr: false }로 해당 Client Component의 서버 사전 렌더링을 제외할 수 있다. App Router에서는 이 선언을 Client Component 안에 두어야 한다. Server Component에서 직접 사용하면 오류가 발생한다. Next.js: Skipping SSR

// app/BrowserPanelLoader.tsx
'use client';

import dynamic from 'next/dynamic';

const BrowserPanel = dynamic(() => import('./BrowserPanel'), {
  ssr: false,
  loading: () => <p>브라우저 정보를 불러오는 중...</p>,
});

export default function BrowserPanelLoader() {
  return <BrowserPanel />;
}
// app/BrowserPanel.tsx
'use client';

export default function BrowserPanel() {
  return <p>현재 브라우저 언어: {navigator.language}</p>;
}

서버 페이지에서는 BrowserPanelLoader를 일반 컴포넌트처럼 import해 사용한다. BrowserPanel의 본문은 브라우저에서 렌더링하고, 서버에서는 대기 UI가 표시될 수 있다. 페이지 전체의 서버 렌더링이나 RSC 사용을 끄는 설정은 아니다.

방식의미대상의 초기 서버 HTML
기본 Server Component서버에서 UI 계산생성에 참여 가능
'use client'클라이언트 모듈 경계 선언기본적으로 생성에 참여 가능
dynamic(() => import(...))동적 로딩이것만으로 SSR을 끄지 않음
dynamic(..., { ssr: false })대상 Client Component의 서버 사전 렌더링 제외대상 본문 제외, 대기 UI 가능

브라우저 전용 라이브러리가 모듈 평가 단계부터 window 등에 의존한다면 이런 경계를 고려할 수 있다. 단순한 브라우저 값 읽기는 앞의 Effect 방식으로 해결할 수도 있다. 서버 HTML에서 제외할 필요가 있는 범위를 기준으로 선택하면 된다. Next.js: Lazy Loading

9. 자주 생기는 오해 바로잡기

오해정확한 설명
Server Component는 SSR 컴포넌트다컴포넌트 실행 모델과 HTML 생성 방식은 별개다
RSC는 요청마다 실행된다빌드 시 실행하거나 생성된 결과를 재사용할 수도 있다
RSC Payload는 HTML이다서버 UI 결과와 클라이언트 참조 등을 담은 직렬화된 표현이다
'use client'가 없으면 무조건 서버 전용이다클라이언트 import 그래프에 포함되는지도 봐야 한다
'use client'는 CSR 전용 선언이다초기 서버 HTML 생성에도 참여할 수 있다
RSC를 쓰면 hydration이 사라진다초기 HTML의 Client Component에는 여전히 필요하다
페이지 이동 때마다 새 HTML과 새 서버 계산이 필요하다Payload와 프리패치·캐시 결과로 갱신할 수 있다
ssr: false는 페이지 전체 SSR을 끈다지정한 Client Component의 서버 사전 렌더링을 제외한다

결론

RSC, SSR, hydration은 각각 역할이 있다. RSC는 서버에서 계산한 컴포넌트 결과를 전달하고, 서버 HTML 렌더링은 초기 화면을 제공하며, hydration은 그 화면에 클라이언트 상호작용을 연결한다.

'use client'는 브라우저에서 실행할 코드의 경계를 정한다. 서버 HTML 생성을 제외해야 하는 컴포넌트에는 dynamic(..., { ssr: false })를 별도로 검토한다. 컴포넌트 코드가 실행되는 곳, HTML이 만들어지는 시점, 브라우저가 UI를 갱신하는 과정을 나누어 보면 이 개념들이 충돌하지 않는다.

공식 문서