프롤로그
최근 면접 진행을 통해 내가 서버 컴포넌트에 대한 내용을 타인에게 자세히 설명하기에는 부족한점이 존재한다는 것을 인지했고 이러한 경험을 통해 좀 더 깊게 파해쳐 보려고한다. 이번에는 서버 컴포넌트(RSC)과 SSR과 함께 이야기를 다루어 본다.
서버 컴포넌트와 SSR의 차이점?
처음 다독다독을 App Router로 마이그레이션 할 때 헷갈렸던 부분이다. 나 처럼 처음 Next.js, App Router, 서버 컴포넌트를 접하는 개발자라면 분명 헷갈릴만한 여지가 있어 보이는 내용이지 싶다.
결론적으론 엄연히 다른 영역이다. 우선 SSR에 대해 간단히 말해보자면 다음과 같다.
"SSR이 동작하는 방식은 서버에서 HTML 뼈대를 렌더링하여 브라우저에 전달한 다음 클라이언트에서 HTML 요소에 하이드레이션(Hydration) 과정을 통해 JS, CSS 요소를 입히는 것"
서버 컴포넌트도 이름 그대로 서버에서 컴포넌트가 렌더링되어 브라우저에 전달하는게 아닌가? 라는 생각, 라는 느낌이 들 수 있다.
하지만 서버 컴포넌트도 클라이언트 컴포넌트와 동일하게 서버에서 HTML 뼈대를 클라이언트에 전달한다는 사실. 그렇다면 도대체 무슨 차이가 있는걸까?
서버 컴포넌트(RSC)는 클라이언트의 JS 번들 사이즈를 줄이기 위한 아키텍처
결론부터 말하면 클라이언트가 서버로부터 받는 서버 컴포넌트에 대한 정보는 HTML 뼈대와 RSC Payload 2가지를 받는다. 클라이언트에서 받는 서버 컴포넌트는 JS를 포함하지 않는다. 그러므로 하이드레이션(Hydration)할 요소도 존재하지 않는다.
서버 컴포넌트를 작성할때 React의 state, hooks, context와 이벤트 리스너 등을 취급할 수 없지만 그래도 JS 요소(조건부 렌더링을 위한 상수나 조건문 작성)는 분명 존재할 수 있다. 그럼에도 단 하나의 JS 없이 HTML 뼈대와 같이 건내받는 RSC Payload는 무엇일까?
RSC Payload는 리액트가 화면을 효율적으로 관리하기위한 UI 청사진으로 생각하면 이해하기 쉽다. 서버 컴포넌트에 작성된 JSX와 JS 코드는 하나의 비대한 JSON 객체(직렬화된 데이터)로 전환된다. 해당 JSON 객체는 React가 화면 트리를 이해할 수 있게 만든 것이며 이게 바로 RSC Payload이다.
그리고 직접 사용해보신 분들은 인지했을 수 있겠지만 처음 페이지에 접근할때만 HTML 뼈대를 받아오고 <Link /> 태그와 같은 요소로 페이지를 이동할때(혹은 <Link /> 컴포넌트에 hover만 해도) 해당 페이지의 RSC Payload를 서버로부터 받아오는 것을 network 탭에서 관찰해볼 수 있다.
이러한 과정 덕에 페이지 이동시 HTML을 다시 받아오지 않고, 새로 받은 RSC Payload를 통해 페이지를 교체하며 페이지 전체가 깜빡거리는 현상을 방지하고 부드럽게 화면이 넘어가는 것을 경험해볼 수 있다.
한줄 요약: RSC Payload는 페이지 이동시 상태를 유지하며 부드럽게 UI 부분 업데이트(React Tree 업데이트)를 하기 위한 특수 직렬화 설계도 데이터다.
이쯤 되면 궁금해지는, App Router는 왜 만들었는가?
Page Router에서 무슨 문제가 있었길래 App Router로 판을 갈아엎었을까? 과거 Page Router 시절로 돌아가보자.
Page Router 시절에는 getServerSideProps(), getStaticProps()를 활용했다. 이러한 방법은 워터폴(Waterfall) 렌더링과 과한 JS 전송이라는 본질적인 한계가 존재했다.
이에 더해 페이지 최상단에서 데이터를 다 가져올 때까지 화면 하단의 UI조차 보여줄 수 없었다. 그리고 페이지 내의 모든 컴포넌트가 하이드레이션(Hydration)의 대상이 되어 수백 kB의 JS를 브라우저에 전달했다.
그래서 Next.js 팀이 문제 해결을 위해 React 18의 RSC를 전격 도입하여 서버 컴포넌트를 만들게 됐다.
- "컴포넌트마다 렌더링 시점을 독립시킨다": 무거운 데이터 로딩이 필요한 컴포넌트만 서스펜스(Suspense) 적용
- 가벼운 헤더나 푸터 HTML은 브라우저로 먼저 쏴버리는 스트리밍(Streaming) 구조로 교체
- 컴포넌트 단위의 zero-bundle-size 달성: 동적인 컴포넌트(클라이언트)와 정적인 컴포넌트(서버)로 분리하여 클라이언트에 전송하는 JS 용량을 축소
다음 글에서는 로그인/로그아웃 상태에 따른 UI를 예시로 서버 컴포넌트와 클라이언트 컴포넌트 작성예시를 작성해보려고한다.
'개발이야기 > Next.js' 카테고리의 다른 글
| [Next.js] 서버 컴포넌트에 대한 오해 (feat. harryk-ds, 다독다독) (0) | 2025.12.25 |
|---|