본문으로 건너뛰기

SSR과 RSC의 차이, RSC Payload와 두 가지 로딩 흐름 정리

·10 min read

React와 Next.js를 공부하면서 가장 오래 헷갈린 게 SSR과 RSC의 관계였다. 둘 다 "서버에서 렌더링한다"는 말로 설명되니 같은 것의 다른 이름처럼 들렸다. 아니었다. 서로 다른 축의 개념이다.

"서버에서 렌더링한다"는 말 하나에 두 개념이 섞여 있었다

SSR(Server-Side Rendering)은 HTML을 서버에서 렌더링하는 방식을 가리킨다. 서버가 HTML을 만들어 브라우저로 보내고, 특히 요청 시점에 그 HTML을 만들면 보통 SSR이라고 부른다.

RSC(React Server Components)는 Server Component를 실행하고 그 결과를 다루는 React의 모델이다. Server Component가 서버에서 실행되고, 그 결과가 RSC Payload가 되어 React 클라이언트로 전달된다.

한쪽은 "무엇을 만들어 보내는가"(HTML), 다른 쪽은 "컴포넌트를 어디서 실행하고 그 결과를 어떻게 다루는가"에 대한 이야기다. 축이 다르니 RSC는 SSR이 아니다.

RSC Payload는 HTML인가?

아니다.

RSC Payload는 HTML이 아니라 Server Component 트리의 렌더 결과를 직렬화한 데이터다. 직렬화는 메모리 위의 구조를 네트워크로 보낼 수 있는 형태로 변환하는 걸 말한다. React 내부 구현에서는 이 직렬화 형식을 Flight라고 부르지만, Next.js 공식 명칭은 RSC Payload다.

HTML은 브라우저가 화면을 구성하기 위한 문서다. RSC Payload는 React가 Server Component 트리를 이해하고 업데이트하기 위한 데이터다. 받는 쪽도 쓰는 목적도 다르다.

App Router도 HTML을 보낸다

RSC가 HTML을 대체하는 건 아니다. App Router의 초기 페이지 로딩에서는 서버가 Server Component를 처리하면서 HTML과 RSC Payload를 함께 만들어 보낸다.

초기 HTML은 Server Component만으로 만들어지지 않는다. Server Component의 렌더 결과인 RSC Payload와 Client Component가 함께 HTML로 프리렌더된다. 프리렌더는 브라우저에 보내기 전에 서버에서 미리 HTML을 만들어두는 걸 말한다.

그래서 브라우저가 받는 건 두 가지다. HTML은 초기 화면을 구성하는 데 쓰이고, RSC Payload는 React가 Server Component 트리를 이해하고 업데이트하는 데 쓰인다.

초기 로딩과 Client Navigation을 갈라놓고 나서야 그림이 맞았다

RSC가 헷갈렸던 이유는 이 둘을 한 덩어리로 보고 있었기 때문이다. 분리해서 봐야 했다.

주소창에 /users를 직접 입력하거나 새로고침하면 Document 요청이 발생한다. 브라우저가 문서부터 받아야 하는 상황이라 여기서는 HTML이 중요하다.

이미 앱이 실행 중인 상태에서 <Link href="/products">를 클릭하면 이야기가 달라진다. Next.js Client Router가 Document 전체를 다시 요청하지 않고, RSC Payload를 받아 React 트리만 업데이트한다.

Network 탭에 RSC 요청이 따로 보이는 이유

DevTools의 Network 탭에서 RSC 관련 요청이 별도로 잡히는 것도 두 흐름이 다른 상황이기 때문이다.

초기 로딩에서는 GET /users가 Document 요청으로 잡히고 응답은 HTML이다. RSC Payload는 그 문서 응답에 함께 스트리밍되거나 문서 안에 포함된 형태로 전달된다. 반면 Client Navigation에서는 RSC Payload를 받기 위한 요청이 독립적으로 발생하고, Network 탭에 별도 요청으로 잡히는 게 이쪽이다.

Client Component가 없어도 RSC Payload는 만들어진다

RSC Payload는 Server Component 트리의 렌더 결과를 직렬화한 데이터이므로, 트리에 Server Component만 있어도 만들어진다. Client Component가 섞여 있으면 거기에 더해, 해당 Client Component를 클라이언트에서 이어서 처리하기 위한 정보와 props가 RSC Payload에 담긴다.

Server Component는 hydration되는가?

아니다. Server Component는 서버에서 실행되고, 그 결과가 HTML과 RSC Payload로 나온다. 브라우저에서 다시 실행될 일이 없으니 hydration 대상이 아니다.

hydration되는 건 Client Component다. Client Component도 초기 로딩에서는 서버에서 HTML로 프리렌더된다. 브라우저는 그 HTML을 먼저 받고, JavaScript가 도착하면 hydration으로 이벤트 핸들러를 붙인다.

RSC의 장점 하나가 여기서 나온다. 서버에서만 필요한 컴포넌트의 코드는 브라우저로 보내서 실행시킬 필요가 없다.

Pages Router에는 이 갈래가 아예 없었다

Pages Router에는 RSC가 존재하지 않는다. 다음과 같은 라우트가 있다고 두고 초기 요청을 따라가 봤다.

pages/
└── users.tsx

요청이 오면 Pages Router가 해당 파일을 찾고, React SSR로 HTML을 만들어 브라우저에 보낸다. 브라우저는 그 HTML을 받고 hydration으로 페이지의 클라이언트 기능을 연결한다. getServerSideProps를 쓰면 그 앞에 데이터 조회 단계가 하나 붙는다. 요청 시점에 서버에서 실행돼 props를 만들어주는 함수다.

Pages Router의 Client Navigation은 JSON을 받아왔다

Pages Router에서도 <Link>를 쓰면 전체 페이지를 새로고침하지 않고 Client Navigation을 한다. 다만 받아오는 게 다르다. Next.js Client Router가 해당 페이지의 Page Data를 JSON으로 받아 React에 넘기고, React가 UI를 갱신한다. RSC Payload를 쓰는 구조가 아니다.

"Pages Router는 SSR, App Router는 RSC"로 외우면 놓치는 것

Pages RouterApp Router
Server Component기본 RSC 모델 없음지원
RSC Payload없음사용
초기 렌더링HTML + JSHTML + RSC Payload 활용
Client NavigationPage Data / JSON 기반RSC Payload 기반
Server / Client Component 분리없음있음
HydrationReact 페이지 전체의 클라이언트 기능 연결Client Component 중심

표만 보면 "Pages Router는 SSR, App Router는 RSC"라고 한 줄로 외우고 싶어진다. 그렇게 외우면 App Router도 여전히 HTML을 만들어 보낸다는 사실이 지워진다. App Router는 RSC라는 React 모델을 Next.js의 라우팅과 렌더링 시스템에 통합한 것이었다.