들어가며
React Server Components(RSC)를 처음 보면 가장 헷갈리는 질문이 하나 생긴다.
이거 결국 SSR이랑 뭐가 다른데?
이 질문에 제대로 답하려면 RSC 자체부터 보기보다, React가 지금까지 어떤 문제를 해결해 왔는지부터 보는 편이 이해가 쉽다. RSC는 갑자기 등장한 새로운 렌더링 기술이라기보다, 클라이언트 중심 React가 커지면서 생긴 비용을 다시 조정하려는 흐름에 가깝다.
React 이전: 서버가 전부 만들던 시대
React 이전에는 PHP 같은 구조가 흔했다. 서버에서 데이터를 가져오고, 바로 HTML을 만들어서 내려주면 끝이었다. 클라이언트는 그 HTML을 받아 화면에 보여주기만 하면 됐다.
이 방식은 단순하다. 하지만 규모가 커질수록 몇 가지 문제가 드러난다.
- UI 로직과 데이터 로직이 서버에 강하게 묶였다.
- 화면의 작은 변화도 서버 렌더링 흐름에 의존했다.
- 트래픽이 늘수록 서버 부담도 함께 커졌다.
React는 이 구조를 깨기 위해 등장했다. 렌더링과 상태 관리를 클라이언트로 가져오면서 프론트엔드는 훨씬 유연해졌고, 컴포넌트 단위로 UI를 구성할 수 있게 됐다.
클라이언트로 다 넘겼더니 생긴 문제
React가 SPA 중심으로 발전하면서 다른 문제가 생겼다. 초기 로딩이 점점 무거워진 것이다.
데이터 가져오기
→ JavaScript 다운로드
→ JavaScript 실행
→ React 렌더링
→ 화면이 interactive 상태가 됨
사용자는 HTML을 받았더라도, 버튼을 누르거나 입력을 할 수 있기까지 기다려야 한다. 화면은 있는 것처럼 보이는데 실제로는 아직 조작할 수 없는 시간이 생긴다. 이 시간이 길어질수록 사용자 경험은 나빠진다.
SSR은 무엇을 해결했나
SSR(Server-Side Rendering)은 이 문제를 완화하기 위해 다시 서버를 활용한다. 서버에서 미리 HTML을 만들어 내려주면 사용자는 빈 화면 대신 완성된 화면을 빠르게 볼 수 있다.
하지만 SSR의 전체 흐름을 보면 여전히 기다리는 구간이 남아 있다.
모든 데이터 fetch
→ HTML 생성
→ JavaScript 다운로드
→ hydration
→ 인터랙션 가능
SSR은 처음 보이는 화면을 빠르게 만들어 준다. 하지만 클라이언트에서 다시 JavaScript를 받고 hydration을 마쳐야 실제 인터랙션이 가능하다. 즉, 화면은 빨리 보이지만 인터랙션 지연 문제는 여전히 남는다.
Suspense와 Streaming으로 나아진 부분
React는 여기서 멈추지 않고 Suspense와 Streaming을 도입했다. 이제는 페이지 전체가 준비될 때까지 기다렸다가 한 번에 보여주는 대신, 준비된 부분부터 먼저 보낼 수 있다.
0.2초: 제목 렌더링
0.5초: 본문 렌더링
1.5초: 댓글 렌더링
체감 성능은 훨씬 좋아진다. 사용자는 전체 응답이 끝날 때까지 빈 화면을 보지 않아도 된다.
다만 구조 자체가 완전히 바뀐 것은 아니다. 많은 경우 데이터 fetch는 서버에서 일어나고, 클라이언트는 여전히 필요한 JavaScript를 다운로드한 뒤 hydration을 수행해야 한다. 그래서 자연스럽게 이런 질문이 나온다.
어차피 서버에서 HTML을 만들고 있다면, 더 많은 일을 서버에서 처리하면 안 되나?
RSC는 여기서 시작한다
React Server Components의 핵심 아이디어는 단순하다.
- 데이터를 가져오는 일은 서버에서 한다.
- 렌더링 가능한 정적인 UI도 서버에서 계산한다.
- 클라이언트는 진짜 인터랙션이 필요한 부분만 담당한다.
즉, 모든 컴포넌트를 클라이언트로 보내서 실행하는 대신, 서버에서 처리할 수 있는 컴포넌트는 서버에 남겨 둔다. 클라이언트에는 상태, 이벤트 핸들러, 브라우저 API처럼 실제로 클라이언트 실행이 필요한 코드만 보낸다.
SSR과 RSC의 가장 큰 차이
SSR과 RSC는 둘 다 서버를 사용한다. 그래서 겉으로 보면 비슷해 보인다. 하지만 핵심 차이는 무엇을 보내느냐에 있다.
- SSR은 JSX를 서버에서 HTML로 바꿔 보낸다. 클라이언트는 그 HTML에 JavaScript를 연결하기 위해 hydration을 수행한다.
- RSC는 HTML이 아니라 React가 이해할 수 있는 컴포넌트 트리 데이터를 보낸다. 클라이언트는 이 데이터를 기존 React 트리와 병합한다.
이 차이가 RSC를 단순한 SSR 개선이 아니라 다른 렌더링 모델로 만든다.
RSC는 HTML이 아니라 트리를 보낸다
RSC의 결과물은 HTML이 아니다. React 전용 직렬화 포맷이다. 보통 Flight 또는 RSC Payload라고 부른다.
아주 단순화하면 이런 형태에 가깝다.
[
[
"$",
"div",
null,
{
"children": [
["$", "h1", null, { "children": "Hello" }],
["$", "ClientRef", "LikeButton", { "likes": 42 }]
]
}
]
]
여기서 중요한 점은 Client Component의 실제 코드가 payload 안에 들어있지 않다는
것이다. 서버는 LikeButton의 구현 코드를 보내는 대신,
여기에 LikeButton이 들어가야 한다는 참조를 보낸다.
덕분에 서버에서 계산 가능한 부분은 서버에서 끝내고, 클라이언트에는 필요한 컴포넌트 코드만 남길 수 있다.
클라이언트는 Payload를 어떻게 처리하나
클라이언트는 RSC Payload를 HTML처럼 그대로 DOM에 붙이지 않는다. React는 이 데이터를 읽어 React Element 트리로 복원하고, 기존 트리와 비교해 병합한다.
- 서버에서 RSC Payload를 스트림으로 받는다.
- React가 payload를 파싱한다.
- 직렬화된 데이터를 React Element 트리로 복원한다.
- 기존 클라이언트 트리와 reconcile한다.
- 변경이 필요한 부분만 DOM에 반영한다.
이 과정이 가능한 이유는 React가 원래부터 DOM을 직접 조작하는 라이브러리라기보다, UI를 트리로 표현하고 그 트리를 기준으로 변경 사항을 계산하는 라이브러리이기 때문이다.
Client Component는 언제 실행되나
RSC Payload 안에는 Client Component에 대한 참조 정보가 포함된다.
{
"type": "client_ref",
"module": "/chunks/LikeButton.js",
"props": {
"likes": 42
}
}
React는 이 정보를 보고 다음처럼 판단한다.
이 컴포넌트는 클라이언트에서 실행해야 하는 컴포넌트구나.
그다음 필요한 JavaScript chunk를 로드하고, 해당 부분에 대해서만 hydration을 진행한다. 모든 컴포넌트를 hydrate하는 것이 아니라, 이벤트 핸들러나 상태처럼 클라이언트 실행이 필요한 컴포넌트만 hydrate하는 것이다.
스트리밍이 중요한 이유
RSC에서 스트리밍이 중요한 이유는 단순하다. 전체 작업이 끝날 때까지 기다리지 않고, 준비된 결과부터 보낼 수 있기 때문이다.
기존 방식:
모든 작업 완료 → 한 번에 응답
스트리밍 방식:
준비된 조각부터 순차적으로 응답
서버에서 빠르게 계산된 UI는 먼저 내려오고, 느린 데이터에 의존하는 영역은 나중에 이어서 내려올 수 있다. 사용자는 더 빨리 의미 있는 화면을 보고, React는 뒤늦게 도착한 조각을 기존 트리에 자연스럽게 병합한다.
결국 RSC의 본질
RSC는 단순히 서버 렌더링을 조금 더 개선한 기술이 아니다. 핵심은 UI를 HTML이 아니라 React 트리 데이터로 전송한다는 점이다.
서버는 데이터 fetch와 렌더링 계산을 맡고, 클라이언트는 전달받은 트리 데이터를 복원해 기존 UI와 병합한다. 그리고 실제 인터랙션이 필요한 Client Component만 JavaScript를 받아 실행한다.
정리
- SSR은 서버에서 만든 HTML을 보내고, 클라이언트가 hydration한다.
- RSC는 HTML이 아니라 React 전용 트리 데이터인 RSC Payload를 보낸다.
- 클라이언트는 payload를 React Element 트리로 복원한 뒤 reconcile한다.
- Client Component는 참조만 전달되고, 필요한 JavaScript chunk만 로드된다.
- 따라서 모든 컴포넌트를 hydrate하지 않고, 인터랙션이 필요한 부분만 hydrate한다.
그래서 RSC를 이해할 때는 "서버에서 렌더링한다"보다 "서버와 클라이언트가 React 트리를 나눠서 만든다"고 보는 편이 더 정확하다.