THE DEVLOG

scribbly.

Next JS 치트 시트

2025.05.09 23:56:48

React 19는 클라이언트 사이드 렌더링에서만 특화되어 있던 기존 방식을 벗어나, RSC(React Server Components)를 정식 스펙으로 채택하며 다음과 같은 핵심 기능을 도입했다.

주요 변화

항목설명
use()서버 컴포넌트에서 Promise(await)을 간단하게 사용할 수 있는 새로운 API
공식 RSC 지원React 자체에서 RSC 처리 및 스트리밍 처리 기본 제공
React Compiler 도입자동 메모이제이션 및 성능 최적화를 위한 컴파일러 (향후 정식)
스트리밍 개선Suspense, RSC, use()와 함께 스트리밍 최적화

서버 컴포넌트 (RSC)

기존 방식 : 모든 컴포넌트는 JS로 번들링됨

React에서 작성된 모든 컴포넌트는 클라이언트로 JS 파일로 번들링되어 브라우저로 전송돼야 실행된다.

// 모든 컴포넌트가 번들됨
function PostList() {
  const posts = usePosts(); // 클라이언트에서 데이터 페칭
  return <ul>{posts.map(p => <li>{p.title}</li>)}</ul>;
}
  • 단점:

    • JS 크기가 커진다 (모든 로직이 포함되므로).
    • 데이터 패칭도 클라이언트에서 일어나 로딩 지연.
    • SEO 불리, 첫 화면 렌더 늦음.

RSC 방삭 : 서버에서 실행되고 결과만 전송됨.

// Server Component
export default async function PostList() {
  const posts = await db.posts.findMany(); // 서버 DB 직접 접근
  return <ul>{posts.map(p => <li>{p.title}</li>)}</ul>;
}
  • 이 컴포넌트는 JS로 번들링되지 않고, 서버에서 실행된 후 HTML이나 RSC Payload 형태로만 클라이언트에 전달됨.
  • 클라이언트는 결과를 렌더링하고, JS 실행은 하지 않는다.
  • 서버 컴포넌트는 서버 내의 DB에 직접 접근하거나 Fetch할 수 있다.

use()

React 19에서 새롭게 도입된 use()await를 컴포넌트 레벨에서 바로 처리하는 함수형 API이다. 서버 컴포넌트에서 데이터 fetch 시 더 간결하고 선언적인 코드를 작성할 수 있다.

async function getData() {
  return fetch("https://api.example.com/posts").then((res) => res.json());
}

export default function Page() {
  const data = use(getData()); // Promise를 바로 처리
  return <div>{data.title}</div>;
}
  • use()는 내부적으로 Suspense 기반이며, Promise를 받을 수 있다.
  • SSR + RSC + 스트리밍과 결합되면 성능 향상 효과가 극대화된다.

RSC Payload

RSC Payload는 React Server Component가 서버에서 실행된 후 생성되는 결과물이다.
이는 HTML이 아니라, React가 이해할 수 있는 JSON-like 포맷의 직렬화된 데이터이며, 다음 특징을 가진다:

  • 컴포넌트 트리를 재구성할 수 있는 데이터 구조
  • React가 이를 받아 클라이언트에서 바로 렌더링 가능
  • Hydration이 불필요한 구조

RSC의 구조는 아래와 같이 개념적으로 표현될 수 있다.

[
  ["$","ul",null,
    ["$","li",null,"첫 번째 항목"],
    ["$","li",null,"두 번째 항목"]
  ]
]

위와 같은 RSC Payload는 브라우저에서 React.createElement로 React.createElement("ul", null, ...)와 같이 재구성하게 된다.

RSC의 렌더링

기존 React SSR에서는 서버가 HTML을 생성하고, 클라이언트는 **같은 구조의 React Element를 다시 만들고 JS 이벤트를 연결(hydrate)**한다.
즉, HTML → JS 구조 재연결이었다.

// 전통적 SSR
const html = ReactDOMServer.renderToString(<App />);
const element = <App />;
hydrateRoot(container, element); // HTML + React Element 매칭

React 19의 RSC 구조에서는 HTML이 없다.

  • 서버는 HTML이 아니라 **React 전용 직렬화 포맷(RSC Payload)**를 전송
  • 클라이언트는 이 Payload를 받아 React Element 트리를 복원
  • 그리고 그걸 기반으로 실제 DOM을 처음부터 생성 (hydrateRoot 사용하지만 본질은 mount)
// RSC 방식
const stream = await fetch("/_rsc");
const element = await createFromReadableStream(stream);
hydrateRoot(container, element); // Payload 기반 마운트

🔍 기존 방식 vs RSC 방식 비교

항목전통적 SSR (React 17~)RSC 기반 방식 (React 19)
서버 응답HTMLRSC Payload (text/x-component)
클라이언트 역할React Element를 만들어 기존 HTML에 hydratePayload → Element → DOM 생성
hydrateRoot 의미기존 HTML + JS 연결Element 기반 DOM 마운트
렌더 성격보통 정적 HTML스트리밍 Element 데이터
JS 번들모든 컴포넌트 포함Client Component만 포함

renderToReadableStream

Node.js는 데이터를 chunk 단위로 읽고, 쓰고, 전달하는 방식. 즉 스트리밍 방식을 주요 기능으로 포함하고 있다.

데이터를 '읽는' readStream과 데이터를 '작성하는' writeSream, 그리고 이 둘 사이를 연결하는 pipe() 함수가 존재한다.

Node.js 서버에서 http.ServerResponse 역시 Writable Stream으로, Node.js는 응답을 스트리밍 방식으로 전달한다.

const readStream = fs.createReadStream("input.txt");
const writeStream = fs.createWriteStream("output.txt");

readStream.pipe(writeStream);

이제 React 19의 renderToReadableStream을 살펴보면, React 19의 RSC Payload는 Readable Stream 형태이다. 그리고 React 19의 서버에서는 이러한 RSC Payload를 Writable Stream인 응답 객체에 담아서 chunk 단위로 전송하게 된다.

const stream = await renderToReadableStream(<App />, webpackMap);
stream.pipe(res); // Node.js res 객체에 바로 연결
  • stream: RSC Payload를 생성하는 ReadableStream
  • res: 브라우저로 응답을 보내는 WritableStream
  • pipe(): chunk를 자동으로 전송

클라이언트에서는 이렇게 chunk 단위로 전송되는 RSC Payload를 받아서 createElement로 Virtual DOM에 추가한다. (즉 JS를 hydration하는 기존 방식이 아닌, RSC Payload를 통해 Virtual DOM을 마운트하는 방식이다.)

const stream = await fetchRscPayloadAsStream();
const element = await ReactServerDOMClient.createFromReadableStream(stream);
ReactDOM.hydrateRoot(container, element);
  • createFromReadableStream()RSC Payload → React Element 트리로 복원
  • hydrateRoot()는 그 Element를 기존 DOM 노드에 이어붙임 (또는 초기 마운트)

이러한 CSR 방식의 렌더링과 RSC의 렌더링 과정을 비교하면 아래와 같다.

  • CSR 방식 : 빈 HTML에 자바스크립트를 이용해 hydration 한다.

    [브라우저 요청]
        ↓
    [서버]  →  index.html (빈 <div id="root">)
        ↓
    [브라우저]  
        → JS 번들 다운로드
        → ReactDOM.render(<App />)
        → API 호출
        → 화면 렌더링
    
  • RSC 방식 : RSC Payload를 스트리밍하고, 이를 이용해 DOM을 만들어 낸다.

    [브라우저 요청]
        ↓
    [Node.js 서버]  
        → renderToReadableStream(<App />)
        → RSC Payload (text/x-component)
        ↓
    [브라우저]  
        → createFromFetch(fetch('/_rsc'))
        → React Element Tree 복원
        → hydrateRoot(container, element)
    
    • createFromFetch를 통해 초기 페이지 로딩 이후에는 아래와 같은 방식으로 createFromReadableStream를 통해 RSC Payload를 스트리밍한다.

      [사용자 상호작용 또는 클라이언트 내 fetch]
          ↓
      [fetch('/_rsc?param=new')]
          ↓
      [서버: renderToReadableStream() → 새 Payload 반환]
          ↓
      [클라이언트]
          → createFromReadableStream(response.body)
          → React Element Tree 복원
          → 기존 컴포넌트 내부에서 상태 업데이트 or <Suspense />로 교체
          → hydrateRoot는 ❌ 다시 호출되지 않음