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) |
---|---|---|
서버 응답 | HTML | RSC Payload (text/x-component) |
클라이언트 역할 | React Element를 만들어 기존 HTML에 hydrate | Payload → 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를 생성하는 ReadableStreamres
: 브라우저로 응답을 보내는 WritableStreampipe()
: 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는 ❌ 다시 호출되지 않음
-