RSC 생성과정 및 SSR 이해

notion image

Network Boundary (Server, Client)

Network Boundary란 client, server 환경을 구분하는 개념적인 선이다.
 
React에서 네트워크 바운더리를 어디에 둘 것인지 선택할 수 있다.
  • data fetch와 유저의 포스트들을 서버에서 렌더링 시킬 수도 있고(Server Components),
  • 좋아요 버튼 등 Interactive한 요소들은 Client에서 렌더링 시킨다. (Client Components)
 
notion image
위 그림의 예시에선 다음과 같이 구분했다.
  • 전 페이지에서 공유되는 Nav 컴포넌트를 서버에서 렌더링 하되,
  • active한 상호작용 가능한 링크들을 보여주는 Links는 client에서 렌더링.

바운더리 분리 방법

Next.js는 기본적으로 SSR로 컴포넌트를 렌더링 한다.
CSR로 렌더링 시키고 싶은 컴포넌트에는 최상단에 ‘use client’ 지시자를 표시한다.
'use client'; import { useState } from 'react'; import { formatDate } from './formatters'; import Button from './button'; export default function RichTextEditor({ timestamp, text }) { const date = formatDate(timestamp); // ... const editButton = <Button />; // ... }
 

RSC 생성 원리

Plasmic BlogPlasmic BlogHow React server components work: an in-depth guide ← 추천!

빌드 과정: 모듈 그래프 생성

React는 컴포넌트가 어떻게 실행될지를 기준으로 두 개의 모듈 그래프를 생성한다.
  1. Server Module Graph (서버 모듈 그래프)
      • server에서 실행될 모든 서버 컴포넌트(Server Component) 를 포함하는 그래프.
      • 여기에는 API 요청, DB 조회, 파일 읽기 등의 서버 전용 코드가 들어갈 수 있음.
      • use client 지시자가 없는 컴포넌트는 자동으로 여기에 포함됨.
      • 서버에서 실행할 수 있도록 준비된 코드의 모음 (요청이 들어오면 서버에서 실행해서 html을 만듦)
        • 실행될 코드들의 레퍼런스들이 Tree 구조로 관리되는 것임.
  1. Client Module Graph (클라이언트 모듈 그래프)
      • use client가 선언된 클라이언트 컴포넌트(Client Component) 들을 포함하는 그래프.
      • 여기에는 이벤트 핸들러, 브라우저 전용 API 사용 코드 등이 들어감.
      • 브라우저에서 실행될 JavaScript 코드로 변환 및 번들링됨. (클라이언트 컴포넌트를 생성할 수 있는 Javscript 코드가 번들링되어 클라이언트 모듈 그래프에 포함됨)
        • 클라이언트에서 실행될 React 컴포넌트의 JavaScript 코드가 미리 번들링되어 포함됨.
        • 이를 위해 Webpack, Vite 같은 번들러가 사용됨.

RSC 생성: 클라이언트 요청 시 동적 처리

notion image
빌드 후 서버는 이 두 개의 모듈 그래프를 유지하고 있다가, 클라이언트가 요청하면 그때 RSC를 생성한다.
📌
RSC로 만들어두지 않는 이유는, 요청받은 시점에서 SSR을 할 때 필요한 api 데이터 등이 달라질 수 있기 때문이다.
  1. 서버에서 서버 컴포넌트를 렌더링하고, 그 결과를 React Server Component Payload (RSC Payload)라는 특별한 데이터 형식으로 변환.
  1. RSC Payload는 클라이언트로 전송되며, 여기에는
      • 서버 컴포넌트의 렌더링 결과 (HTML)
      • 클라이언트 컴포넌트가 들어가야 할 자리(Placeholder)
      • 클라이언트 컴포넌트가 실행할 번들된 JavaScript 파일의 경로(참조정보)가 포함됨.
  1. 클라이언트의 React가 RSC Payload를 받아서
      • 서버에서 받은 HTML을 바탕으로 초기 렌더링 수행.
      • 클라이언트 컴포넌트가 필요한 위치에 JavaScript 번들을 로드하고 실행하여 DOM을 업데이트.

예시

코드

// 예제: 페이지 컴포넌트 export default function Page() { return ( <Layout> <ServerComponent /> <ClientComponent /> {/* use client */} </Layout> ); } // 서버 컴포넌트 function ServerComponent() { return <div>서버에서 렌더링됨</div>; }

Server module graph

Page ├── Layout (Server) // 실행할 코드 참조 └── ServerComponent (Server) // 실행할 코드 참조

client module graph

// 🚨 빌드 시점에서 컴포넌트를 그릴 javascript 코드로 번들링 되어 있음. Page └── ClientComponent (Client) // 번들링된 클라이언트 컴포넌트의 참조

서버 처리 (RSC 생성)

사용자로부터 요청을 받으면, 빌드 시점에 생성한 server module graph와 client module graph를 참조해서 RSC를 생성함.
// server component { $$typeof: Symbol(react.element), type: "div", key: null, ref: null, props: { title: "oh my", children: "서버에서 렌더링됨" }, _owner: null, _store: {} } // 이후 RSC Payload로 직렬화 되어 아래와 같이 변환됨. // json [ { "type": "div", "props": { "title": "oh my", "children": "서버에서 렌더링됨" } } ]
// client component { $$typeof: Symbol(react.element), type: { $$typeof: Symbol(react.module.reference), // CSR 필요한 건 레퍼런스를 남김 // ClientComponent is the default export... name: "default", filename: "./src/ClientComponent.client.js" // 파일 위치 정보 }, props: { children: "oh my" }, } // 이후 RSC Payload로 직렬화 되어 아래와 같이 변환됨. // json [ { "$$typeof": "react.element", "type": { "$$typeof": "react.module.reference", "name": "default", "filename": "./src/ClientComponent.client.js" }, "props": { "children": "oh my" } } ]

서버처리 (SSR)

  • 서버에서 RSC Payload를 사용하여 초기 HTML을 렌더링.
  • Server Component는 미리 HTML로 변환해서 응답에 포함됨.
  • 반면, Client Component는 실행되지 않고, placeholder 또는 boundary 마커만 남김.
<html> <body> <div title="oh my">서버에서 렌더링됨</div> <!-- React Server Component ClientBoundary: ClientComponent --> </body> </html>

클라이언트 처리 (초기 렌더링)

  • 클라이언트는 서버로부터 HTML + RSC Payload를 받음.
  • 서버로부터 SSR 된 html을 받아서 초기 렌더링.

클라이언트 처리 (Hydration)

  • SSR 결과를 interactive 하게 만드는 과정.
  • 클라이언트가 받은 HTML에 React를 연결하여 Interactive하게 만듦
  • React는 기존 DOM을 파싱하여 React 내부 구조로 변환하고 이벤트 핸들러를 다시 연결함.
📌
Hydration의 목적은 SSR로 생성된 정적인 HTML을 React가 다시 컨트롤할 수 있도록 만드는 것
  • 정적 html로 사용자에게 바로 보여주고 (정적인 페이지),
  • 이후 hydration 과정을 통해 react가 제어할 수 있도록 하는 것 (이벤트 연결 등)
    • i.g. button 요소가 초기 html 렌더링 시에는 UI만 보여짐. 하이드레이션 과정에서 이벤트 핸들러를 연결하며 interactive 하게 됨.

클라이언트 처리 (CSR)

  • RSC Payload를 파싱하여 React.createElement()를 통해 다시 React 요소로 변환.
  • 클라이언트가 번들링된 JS 파일을 다운로드해서 실행.
    • type 필드에 있는 react.module.reference를 보고, 해당 파일의 JS를 로드한 후 컴포넌트를 실행
// 이 과정은 React 내부에서 자동으로 이루어짐 import("./src/ClientComponent.client.js").then((mod) => { const ClientComponent = mod.default; React.createElement(ClientComponent, { children: "oh my" }); });
📌
CSR은 클라이언트에서 추가적인 JavaScript 로직을 실행하는 과정이며,
Hydration과 CSR은 병렬적으로 일어날 수 있다
 

요약

1️⃣ 빌드 과정:
  • React는 server module graphclient module graph를 나누어 관리함.
    • ✅ Server Module Graph 서버에서 실행할 수 있도록 준비된 코드의 모음
      • 요청 시점에 서버에서 실행하여 html을 만듦.
    • ✅ Client Module Graph 브라우저에서 실행할 JavaScript 코드로 번들링됨
      • client에서 컴포넌트를 만들 떄 필요한 javascript를 미리 번들링 해놓음.
2️⃣ 클라이언트가 요청하면:
  • 서버에서 server module graph를 실행해 RSC Payload를 생성함.
3️⃣ 클라이언트는:
  • 서버에서 받은 HTML을 사용해 초기 렌더링.
  • 필요한 JavaScript를 로드하여 클라이언트 컴포넌트를 실행하고 DOM을 업데이트.