RSC가 번들러와 통합되는 이유는

I
Inkyu Oh

Front-End2025.11.19

overreacted 블로그 포스트 번역


공정한 경고—이것은 너드들을 위한 글입니다.
React Server Components는 모듈 시스템을 확장하여 서버/클라이언트 애플리케이션을 두 개의 런타임에 걸쳐 있는 단일 프로그램으로 표현하는 프로그래밍 패러다임입니다. 내부적으로 RSC 구현은 두 가지 주요 부분으로 구성됩니다:
react-serverreact-client 패키지는 React 저장소 내부에 있습니다.
물론 완전히 오픈 소스이지만, npm에 원본 형태로 게시되지 않습니다. 이는 핵심 요소가 부족하기 때문입니다—모듈 시스템 통합입니다. 많은 (de)serializer와 달리 RSC는 데이터를 보내는 것뿐만 아니라 코드를 보내는 것도 관심을 가집니다. 예를 들어, 이 트리를 생각해봅시다:
<p>Hello, world</p>
<p> 태그를 JSON으로 변환하려면 다음과 같이 할 수 있습니다:
{
type: 'p',
props: {
children: 'Hello world'
}
}
하지만 이제 이 <Counter> 태그를 생각해봅시다. 어떻게 serialize할까요?
import { Counter } from './client';

<Counter initialCount={10} />
'use client';

import { useState, useEffect } from 'react';

export function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
// ...
}
어떻게 모듈을 serialize할까요?



Serializing Modules

우리는 와이어의 다른 쪽에서 실제 <Counter>를 복원하고 싶다는 것을 기억하세요—따라서 우리는 단지 스냅샷을 원하는 것이 아닙니다. 우리는 상호작용을 위한 전체 로직을 원합니다!
이를 serialize하는 한 가지 방법은 Counter 코드를 JSON에 직접 포함시키는 것입니다:
{
type: `
import { useState, useEffect } from 'react';

export function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
// ...
}
`,
props: {
initialCount: 10
}
}
하지만 이것은 좀 나쁘지 않나요? 당신은 정말로 코드를 문자열로 클라이언트에 eval로 보내고 싶지 않으며, 같은 컴포넌트의 코드를 여러 번 보내고 싶지 않습니다. 대신 그 코드가 우리 앱에 의해 정적 JS 자산으로 제공되고 있다고 가정하는 것이 합리적입니다—이를 JSON에서 참조할 수 있습니다. 거의 <script> 태그 같습니다:
{
type: '/src/client.js#Counter', // "Load src/client.js and grab Counter"
props: {
initialCount: 10
}
}
실제로 클라이언트에서는 <script> 태그를 생성하여 로드할 수 있습니다.
그러나 네트워크를 통해 소스 파일에서 import를 하나씩 로드하는 것은 비효율적입니다. 한 파일이 다른 파일을 import할 수 있고, 클라이언트가 import 트리를 미리 알지 못한다는 것을 기억하세요. 당신은 waterfall을 만들고 싶지 않습니다. 우리는 이미 클라이언트 측 애플리케이션 작업의 2년에 걸쳐 이를 해결하는 방법을 알고 있습니다: bundling입니다.



RSC Bundler Bindings

이러한 이유로 RSC는 bundler와 통합됩니다. RSC는 엄밀히 말해 bundler를 요구하지 않습니다: bundler 없는 RSC ESM 개념 증명이 있습니다. 하지만 실제로 더 많은 최적화 없이 순진하게 하는 것이 얼마나 비효율적인지 때문에 대부분 역사적 이유로 존재합니다.
현실적인 RSC 통합은 bundler 특정적입니다. Parcel, Webpack, 그리고 (결국) Vite에 대한 바인딩은 React 저장소에 있으며 모듈을 보내고 로드하는 방법을 지정합니다:
  • 먼저, 빌드 중에, 그들의 작업은 'use client'가 있는 파일을 찾고 실제로 해당 진입점에 대한 번들 청크를 생성하는 것입니다—Astro Islands와 조금 비슷합니다.
  • 그 다음, 서버에서, 이러한 바인딩은 React에 모듈을 클라이언트에 보내는 방법을 가르칩니다. 예를 들어, bundler는 'chunk123.js#Counter'와 같은 모듈을 참조할 수 있습니다.
  • 클라이언트에서, 그들은 React에 bundler 런타임에 해당 모듈을 로드하도록 요청하는 방법을 가르칩니다. 예를 들어, Parcel 바인딩은 Parcel 특정 함수를 호출합니다.
이 세 가지 덕분에 React Server는 모듈을 만날 때 serialize하는 방법을 알 것이고—React Client는 deserialize하는 방법을 알 것입니다.
React Server로 트리를 serialize하는 API는 bundler 바인딩을 통해 노출됩니다:
import { serialize } from 'react-server-dom-yourbundler'; // Bundler-specific package

const reactTree = <Counter initialCount={10} />;
const outputString = serialize(reactTree); // Something like the JSON above
그 다음 outputString을 디스크에 저장하거나, 네트워크를 통해 보내거나, 캐시하거나, 무엇이든 할 수 있습니다—그리고 결국 React Client에 공급합니다. React Client는 전체 트리를 deserialize하고, 필요에 따라 참조된 모듈에서 코드를 로드합니다:
import { deserialize } from 'react-server-dom-yourbundler/client'; // Bundler-specific package

const outputString = // ... received over network, read from disk, etc...
const reactTree = deserialize(outputString); // <Counter initialCount={10} />
그리고 그것은, 모든 것이 올바르게 작동했다고 가정하면, 마치 당신이 클라이언트에서 직접 <Counter initialCount={10} />을 작성한 것처럼 일반적인 JSX 조각을 줄 것입니다. 당신은 그 트리로 무엇이든 할 수 있습니다—렌더링하고, 상태에 유지하고, HTML로 변환하는 등:
const outputString = // ... received over network, read from disk, etc...
const reactTree = deserialize(outputString); // <Counter initialCount={10} />

// You can do anything you'd do with a regular JSX tree, for example:
const root = createRoot(domNode);
root.render(reactTree);
이것이 Next.js와 같은 RSC 프레임워크가 내부적으로 사용하는 API입니다.
RSC를 이러한 낮은 수준의 API를 사용하여 재생하고 React 트리가 (de)serialize되는 것을 보고 싶다면, Parcel RSC 구현이 좋은 시작점입니다.
(위의 serializedeserialize 이름은 설명적입니다. 정확한 이름은 바인딩에 따라 다르며 여러 오버로드가 있을 수 있습니다. 예를 들어, 기본 react-server-dom-parcel 바인딩에 대한 얇은 래퍼인 @parcel/rsc 패키지는 serialization을 renderRSC로, deserialization을 fetchRSC로 노출합니다. 또한 실제 구현은 non-blocking이며 양쪽에서 streaming을 지원합니다.)
0
7

댓글

?

아직 댓글이 없습니다.

첫 번째 댓글을 작성해보세요!