0biglife.

[Next.js] 리액트 서버 컴포넌트(RSC)는 어떻게 렌더링되는가

Frontend/Next.js

· 2025-04-27

[Next.js] 리액트 서버 컴포넌트(RSC)는 어떻게 렌더링되는가

들어가며

SSR 구현 과정에 대해 게시글을 쓰다가 서버 컴포넌트에 대한 설명이 길어져 분리해서 새 글을 작성한다. React Serve Component(RSC)는 서버에서 실행되는 특별한 컴포넌트다. Next.js 13부터 도입된 서버 컴포넌트는 기존 클라이언트 렌더리방식과는 전혀 다른 렌더링 방식을 가지기 때문에 이를 정리해보고자 한다. 이 게시글은 Next.js 공식 문서와 해외 블로그를 참고해서 작성되었다. 이 게시글을 보고 '서버 컴포넌트가 어떻게 렌더링되는지'와 '서버 컴포넌트 덕분에 초기 로드 속도가 왜 빨라졌는지'를 명확하게 알아가길 바란다. 시작해보자!

컴포넌트란?

서버 컴포넌트 렌더링을 이해하기 전에, 컴포넌트가 뭔지 가볍게 짚고 넘어가자. 컴포넌트는 데이터(props)를 인자로 받아서 JSX를 반환하는 자바스크립트 함수다. 이 컴포넌트는 "렌더링"되기 위해 다음 과정을 거친다.

  1. 반환된 JSX가 Babel에 의해 트랜스파일링되어 React.createElement 형태로 변환된다.

  2. 그 결과 JSX가 React Element라는 JS 객체가 된다. 이 때, 이 객체는 DOM에 표현될 필요 정보들을 담고 있다.

  3. React는 이를 기반으로 Fiber라는 구조로 만들어 Virtual DOM 트리를 구성한다.

  4. 최종적으로 실제 DOM에 반영시킨다.

즉, "컴포넌트가 렌더링된다"는 것은 단순히 함수가 호출되는 것이 아니라, JSX -> React Element -> Fiber Tree -> Virtual DOM -> 실제 DOM이라는 파이프라인이 동작하는 것이다.

서버 컴포넌트(RSC)

클라이언트 컴포넌트는 브라우저에서 모든 로직을 처리한다. 그렇기 때문에 JS 번들 크기가 커짐에 따라 느려지는 FCP(First Contentful Paint), SEO 문제 등을 해결하고자 React는 서버에서 렌더링 가능한 컴포넌트라는 새로운 개념을 도입했다.

렌더링 구조

Next.js Doc.Next.js Doc.

Next.js는 React를 위한 프레임워크이며, 이는 곧 React를 활용해서 무언가를 한다는 것을 의미한다. 문서에 있는 말을 빌리자면, "On the server, Next.js uses React's APIs to orchestrate rendering. The rendering work is split into chunks: by individual route segments and Suspense Boundaries." 즉, Next.js는 React API를 통해 렌더링을 조정하며, 이 때 렌더링 작업은 Route SegmentSuspense Boundary를 기준으로 chunk로 나뉘어진다.

  • Route Segment: /app 디렉토리 하위에 page.tsx, layout.tsx와 같은 특정 파일 단위로 나뉘는 구간

  • Suspense Boundary: React.Suspense로 감싸진 비동기 컴포넌트 블록

이렇게 나뉘어진 각 렌더링 블록은 서버에서 React에 의해 실행되고, React는 이를 RSC Payload라는 특수한 데이터 포맷으로 출력한다.

RSC Payload

React는 서버 컴포넌트를 React Server Component Payload(RSC Payload) 형태로 렌더링한다고 했다. 그리고 문서에서는 이 RSC Payload를 독특한 데이터 포맷이라고 했다. 구성요소를 살펴보면, 총 네 가지를 담고 있다.

  • 서버 컴포넌트의 렌더링 결과

  • 클라이언트 컴포넌트가 어디에 렌더링될지에 대한 정보(우리는 이 빈 공간을 Placeholder라고 칭하겠다)

  • 클라이언트 컴포넌트의 JS 참조 경로

  • 서버 컴포넌트가 클라이언트 컴포넌트에 전달해야 하는 props

위 구성요소를 통해 서버에서 이 Payload를 생성한 뒤, 클라이언트가 이(Payload)를 기반으로 컴포넌트 트리를 완성할 수 있도록 넘겨준다. 나온 김에 설명한 내용이지만 사실 이 과정이 서버 컴포넌트가 렌더링되는 첫 번째 스탭이 된다.

서버 컴포넌트 렌더링 과정

서버 측 동작

다음 두 단계를 통해 서버 측 동작이 이루어진다.

1. RSC Payload 생성

React가 서버 컴포넌트를 렌더하며 이 과정에서 RSC Payload를 만든다.

2. HTML 생성

Next.js가 React로부터 만들어진 RSC Payload와 클라이언트 컴포넌트 자바스크립트 인스트럭션(JS 명령어 뭉치)을 조합하여 HTML을 생성한다. 단, 이 때 HTML은 Placeholder를 남겨둔 상태이다.

JS 인스트럭션

JS 인스트럭션은 useState, onClick과 같은 클라이언트 기능을 가능하게 하는 JS 코드 조각이다. 서버는 이 HTML과 함께 필요한 JS 조각을 준비해둔다.

여기까지 서버에서 벌어지는 일이다. RSC Payload로 만들어진 HTML이 준비가 되었다면, 이제 클라이언트에서는 이걸 받아서 다음 동작을 실행한다.

클라이언트 측 동작

클라이언트에서는 다음과 같은 순서도 렌더링을 완성시킨다.

1. HTML 즉시 렌더링

브라우저에서는 서버에서 받은 HTML을 그대로 그려 보여준다. 그러기 때문에 사용자 입장에서는 바로 관측 가능하여 초기 로드 속도가 빠른 효과가 있는 것이다.

2. Reconciliation(조정 단계)

React는 RSC Payload를 기반으로 클라이언트 컴포넌트를 다시 구성한다. 이 과정에서 앞에서 남겨준 클라이언트 컴포넌트 빈자리인 Placeholder를 채워준다. 이 과정을 통해 클라이언트 컴포넌트 트리와 서버 컴포넌트 트리를 재조정하여 결과적으로 리액트 컴포넌트 트리가 만들어진다. 그 결과, Virtual DOM이 실제 DOM에 업데이트를 하게 된다.

3. Hydration(인터랙션 부여)

1단계에서 HTML을 즉시 보여주고, 2단계에서는 리액트 컴포넌트 트리를 구성하여 placeholder를 채웠다. 이제는 이 채워진 빈자리(placeholer)에 인터랙션에 가능하도록 자바스크립트 인스트럭션을 활용해서 hydrate라는 것으로 서버 컴포넌트 렌더링이 마무리된다.

세부적으로는 다음 작업들로 이루어진다.

  • useState, useEffect 등의 hook 동작

  • 버튼 클릭, 인풋 입력 등의 인터랙션 활성화

위 과정을 적용함으로써 이제 사용자와의 상호작용이 가능해진다.

요약

React Server Component(RSC)는 단순히 서버에서 렌더링되는 컴포넌트가 아니라. 브라우저와 서버의 역할을 명확하게 분리함으로써 JS 번들을 줄이고 성능을 개선시키려는 React의 근사한 구조가 아닐까 싶다.

간단히 요약해보면,

  • 서버 동작 : React가 (서버)컴포넌트를 렌더링하여 RSC Payload 생성 → Next.js가 HTML과 함께 클라이언트용 JS 번들을 구성

  • 클라이언트 동작 : 서버에서 완성된 HTML을 화면에 즉시 렌더링 → RSC Payload 기반으로 재조정 → Hydration으로 인터랙션 추가

실제 예제

이제 컴포넌트가 렌더링됨에 따라 JSX, Hydrate 등의 동작을 직접 개발자도구에서 찾아보자. Next.js 13부터는 서버 컴포넌트가 기본값으로 설정되어 있다. 따라서, app 디렉토리 하위에 있는 컴포넌트들은 모두 서버 컴포넌트로 렌더링된다. 클라이언트 컴포넌트로 렌더링하고 싶다면, use client를 추가해주면 된다.

1"use client";
2
3export default function Home() {
4  const [value, setValue] = useState<number>(0);
5
6  const handleClick = () => {
7    console.log("tapped!");
8    setValue((prev: number) => prev + 1);
9  };
10
11  return (
12    <div>
13      <h1>Home</h1>
14      <h2>Count: {value}</h2>
15      <button onClick={handleClick}>{value}</button>
16    </div>
17  );
18}

이렇게 Home 이라는 클라이언트 컴포넌트를 간단히 루트에 선언해주고, yarn build를 해주자. 그러면 .next 폴더가 생성될테고 yarn start로 서버를 실행하면 localhost:3000으로 호스팅되어 접속할 수 있다.

브라우저 개발자 도구 → localhostPreview 탭을 보면 뼈대만 존재하는 HTML이 보인다. 이 시점에서는 서버에서 React가 TSC Payload를 만들고, Next.js가 JS 인스트럭션을 붙여 생성한 HTML만 렌더링되었기 때문에 버튼 클릭(인터랙션)이 동작하지 않는다.

js instruction pathjs instruction path

앞에서 JS 인스트럭션은 useState, onClick과 같은 클라이언트 기능을 가능하게 하는 JS 코드 조각이라고 했다. 이는 콘솔 처리해둔 tapped!를 vscode 상에 검색해보면, ./next/standalone/.next/server/app/page.js 안에 있다(필자는 output 설정을 standalone으로 해두었기에 해당 경로에 있어 다른 분들과는 다를 수 있다). 게다가, 루트 page.tsx에서 쓰이는 JS 인스트럭션 파일(.js)도 동일한 경로에 있는 것을 확인할 수 있다.

이제 클라이언트에서 Reconciliation 단계를 통해 RSC Payload를 받아서 컴포넌트 트리를 만들고, 빈 자리에 Placeholder를 채웠을테고, 위 사진처럼 tapped!가 검색되었던 js 인스트럭션 코드 조각을 받아와서 Hydrate을 하고, onClick이 이제 버튼에 붙어서 할당된 흐름이다. 이 시점부터 이제 우리는 브라우저 상에서 버튼 클릭과 같은 인터랙션이 가능한 것이다.

마치며

이렇게 서버 컴포넌트가 뭔지, 어떻게 렌더되는지 알아보았다. 이제야 비로소 SSR에 대해 정리할 차례가 된 듯하다.

Index

들어가며컴포넌트란?서버 컴포넌트(RSC)렌더링 구조RSC Payload서버 컴포넌트 렌더링 과정서버 측 동작클라이언트 측 동작요약실제 예제마치며