이 글은 how-react-server-components-work에 대한 글을 번역 및 추가 설명한 글입니다.
지난 2020년 12월, React Server Components가 소개되었지만React 18의 initial release에는 포함되지 않았습니다. 이 Next.js13의 SSR을 보다가 궁금해져서 알아본 글 입니다.
React 서버 컴포넌트를 사용하면 서버와 클라이언트(브라우저)가 협업하여 React 애플리케이션을 렌더링할 수 있습니다. 페이지에 대해 렌더링되는 일반적인 React 엘리먼트 트리를 생각해 보세요. 이 트리는 일반적으로 더 많은 React 컴포넌트를 렌더링하는 여러 React 컴포넌트로 구성됩니다. RSC를 사용하면 이 트리의 일부 컴포넌트는 서버에서 렌더링하고 일부 컴포넌트는 브라우저에서 렌더링할 수 있습니다.
다음은 서버에서 렌더링되는 주황색 컴포넌트와 클라이언트에서 렌더링되는 파란색 컴포넌트가 있는 React 트리를 통해 최종 목표가 무엇인지 보여주는 React 팀의 간단한 그림입니다.

React Server Components(RSC)와 Server Side Rendering(SSR)의 차이점과 관계
하지만 이 글의 목적은 SSR보다는 순전히 RSC에 집중하는 것입니다.
요약하자면, React Server Components(RSC)와 Server Side Rendering(SSR)은 모두 "서버"에서 작업을 수행하지만 각각 별개의 기능으로 동작합니다. 하지만 필요에 따라 이들을 결합하여 사용할 수도 있습니다.
React Server Components(RSC)와 클라이언트 컴포넌트의 관계를 설명하려면, 위의 컵케이크 예시를 다음과 같이 비유할 수 있습니다.
클라이언트에서 모든 작업을 처리하는 것은 마치 아이들에게 처음부터 컵케이크를 만들고 장식하라고 하는 것과 같습니다. 클라이언트는 데이터 로딩, 상태 관리 등의 복잡한 과정을 거치며, 이 과정은 많은 시간과 리소스를 소모합니다. 이는 밀가루와 설탕, 버터 등 원재료를 아이들에게 주고 오븐을 사용하도록 하며 많은 설명서를 읽어주는 것과 유사합니다.
반면에 RSC는 서버에서 미리 일부 작업을 처리하는 방식입니다. 서버에서 데이터 가져오기, 렌더링 최적화 등의 작업을 미리 수행하므로 클라이언트가 할 일이 줄어듭니다. 이는 어른(서버)가 먼저 컵케이크를 굽고 프로스팅을 만드는 것과 비슷합니다.
결국 RSC를 사용함으로써 클라이언트(아이들)는 복잡한 과정 없이 직접적으로 중요한 작업인 UI 업데이트(컵케익 장식)에만 집중할 수 있습니다. 그 결과 전체적인 애플리케션 성능 개선 및 사용자 경험 향상(빠르게 컵커테익 장식하기)을 기대할 수 있으며, 부담 없는 코드 구조(오븐 사용 걱정 없음)도 가능해집니다.

React Server Components(RSC)는 웹 개발에서 효율적인 분업을 가능하게 합니다. 서버가 자신의 강점을 활용해 처리할 수 있는 작업을 먼저 수행하고, 나머지는 클라이언트에게 넘겨 마무리하는 방식입니다. 이런 접근법은 서버의 부담을 줄이고, 전체적으로 더 효육적으로 데이터를 처리할 수 있게 해줍니다.
React 애플리케이션에서 일부 컴포넌트는 서버에서 렌더링되고, 일부는 클라이언트에서 렌더링되도록 구성될 수 있습니다. 서버는 평소처럼 서버 컴포넌트를 "렌더링"하여 React 컴포넌트를 div, p 등의 HTML 요소로 변환합니다. 그러나 클라이언트에서 렌더링해야 하는 "클라이언트" 컴포넌트를 만나면, 해당 위치에 대한 표시자(placeholder)만 출력하며 실제 구현은 클라이언트에게 위임합니다.
클라이언트 쪽으로 넘어가면 이 placeholder와 함께 오는 지침에 따라 올바른 클라이언트 컴포넌트와 속성(props)으로 해당 위치를 채웁니다. 그러면 완성된 페이지가 사용자 앞에 표시됩니다.
실제로 이 과정은 좀 더 복잡하며 세부 사항들도 많지만, 이 설명은 RSC의 기본 개념과 작동 방식에 대한 좋은 시작점입니다. 추후 포스트에서는 이러한 세부 사항들을 좀 더 깊게 다룰 예정입니다.
React Server Components(RSC)는 그 자체로 새로운 개념이지만, 기존의 클라이언트 컴포넌트와 어떻게 구분하고 관리할 수 있을까요? React 팀은 이를 위해 매우 실용적인 방법을 제안했습니다: 파일 확장자를 기준으로 컴포넌트의 유형을 결정하는 것입니다.
서버 컴포넌트는 .server.jsx 확장자를 가진 파일에 작성되며, 클라이언트 컴포넌트는 .client.jsx 확장자를 가진 파일에 작성됩니다. 만약 둘 다 해당하지 않는 경우, 그것은 서버와 클라이언트 양쪽에서 모두 사용 가능한 공유 컴포넌트로 간주됩니다.
이 방식의 장점은 개발자와 번들러 모두가 쉽게 구분할 수 있다는 점입니다. 번들러는 파일 이름을 확인하여 필요한 처리를 하는 등, RSC 도입으로 인해 중요한 역할을 하게 됩니다.
그러나 주의해야 할 점은, 서버 컴포넌트와 클라이언트 컴포넌트 사이에 명확한 경계가 있다는 것입니다. 특히 클라이언트 컴포넌트에서 서버 컴포넌트를 가져오려고 하면 안 됩니다. 왜냐하면 서버 컴포넌트는 브라우저에서 실행되지 않으며, 브라우저에서 작동하지 않을 수 있는 코드가 포함될 수 있기 때문입니다. 만약 이런 종속성이 발생한다면 문제가 될 수 있습니다.
따라서 이 점에 주의하며, 각각의 역할과 범위 내에서 서버와 클라이언트 사이드 코드를 효과적으로 분리하고 관리하는 것은 RSC 도입 시 중요합니다.
이 마지막 부분은 조금 혼란스러울 수 있습니다. 이는 특정 클라이언트 컴포넌트의 구성이 문제가 될 수 있다는 것을 의미합니다
// ClientComponent.client.jsx
// NOT OK:
import ServerComponent from './ServerComponent.server'
export default function ClientComponent() {
return (
<div>
<ServerComponent />
</div>
)
}
하지만 클라이언트 컴포넌트가 서버 컴포넌트를 가져올 수 없고, 따라서 서버 컴포넌트를 인스턴스화할 수 없다면 어떻게 서버와 클라이언트 컴포넌트가 서로 끼어있는 이런 React 트리를 만들 수 있을까요? 어떻게 클라이언트 컴포넌트(파란색 점) 아래에 서버 컴포넌트(주황색 점)를 가질 수 있을까요?
클라이언트 컴포넌트에서 서버 컴포넌트를 가져와서 렌더링할 수는 없지만 컴포지션을 사용할 수 있습니다. 즉, 클라이언트 컴포넌트는 불투명한 React노드인 소품을 가져올 수 있고, 그 React노드는 서버 컴포넌트에 의해 렌더링될 수 있습니다. 예를 들어 보겠습니다.
// ClientComponent.client.jsx
export default function ClientComponent({ children }) {
return (
<div>
<h1>Hello from client land</h1>
{children}
</div>
)
}
// ServerComponent.server.jsx
export default function ServerComponent() {
return <span>Hello from server land</span>
}
// OuterServerComponent.server.jsx
// OuterServerComponent can instantiate both client and server
// components, and we are passing in a <ServerComponent/> as
// the children prop to the ClientComponent.
import ClientComponent from './ClientComponent.client'
import ServerComponent from './ServerComponent.server'
export default function OuterServerComponent() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
이 제한은 RSC를 더 잘 활용하기 위해 컴포넌트를 구성하는 방식에 큰 영향을 미칩니다
💡요약
React Server Components(RSC)의 사용법과 제한사항이 우리가 컴포넌트를 구성하고 사용하는 방식에 큰 영향을 미친다는 것을 의미합니다.
클라이언트 컴포넌트에서 서버 컴포넌트를 직접 가져와서 렌더링할 수 없기 때문에, 대신 컴포지션(composition)을 이용해야 합니다. 즉, 클라이언트 컴포넌트는 자식(children) 속성으로 React 노드를 받아들일 수 있고, 이 React 노드는 서버에서 렌더링될 수 있습니다.
위의 코드 예시에서 OuterServerComponent는 클라이언트와 서버 모두의 컴포넌트를 생성하며, ClientComponent에게 자식으로 ServerComponent를 전달합니다. 이렇게 하면 클라이언트 쪽에서는 서버 컴포넌트의 내부 구현에 대해 알 필요 없이 해당 부분을 그대로 받아들여 사용할 수 있습니다.
React 서버 컴포넌트를 렌더링하려고 할 때 실제로 어떤 일이 일어나는지 자세히 살펴보겠습니다. 서버 컴포넌트를 사용하기 위해 여기에 있는 모든 내용을 이해할 필요는 없지만, 어떻게 작동하는지에 대한 이해도를 높일 수 있습니다.
서버는 들어오는 요청에 포함된 정보를 바탕으로 어떤 서버 컴포넌트를 사용하고, 어떤 속성(props)을 적용할지 결정합니다. 대체적으로 이 요청은 특정 URL에 대한 페이지 요청 형태로 제공되지만, Shopify Hydrogen과 같은 특정 시스템들은 더 세분화된 방법을 제공하며, React 팀의 공식 데모에서도 기본적인 구현을 볼 수 있습니다.
여기서의 최종 목표는 초기 루트 서버 컴포넌트를 기본 HTML 태그와 클라이언트 컴포넌트 'placeholders'의 트리로 렌더링하는 것입니다. 이렇게 렌더링된 트리는 직렬화되어 브라우저로 전송됩니다.
브라우저에서는 이 직렬화된 트리를 역직렬화하고, 클라이언트 '플레이스홀더'를 실제 클라이언트 컴포넌트로 교체합니다. 그 후, 최종 결과물을 렌더링하여 사용자에게 보여줍니다.
간단하게 말해, 서버에서 생성된 기본 HTML과 클라이언트 컴포넌트의 틀(placeholder)을 가진 페이지가 초기에 로드되고, 이 후 실제 클라이언트 컴포넌트가 그 자리를 차지하면서 완전한 페이지로 변환되는 과정을 설명한 것입니다.
직렬화린?
직렬화(serialization)는 컴퓨터 과학에서 데이터 구조나 객체 상태를 저장하거나 전송할 수 있는 형식으로 변환하는 과정을 말합니다.
이 변환된 형식은 나중에 다시 원래의 데이터 구조나 객체로 복원(deserialization, 역직렬화)할 수 있습니다.
위의 예시에 따라 OuterServerComponent를 렌더링하고 싶다고 가정해 보겠습니다. JSON.stringify(OuterServerComponent)를 수행하여 직렬화된 엘리먼트 트리를 가져올 수 있을까요?
거의 비슷하지만 아직은 아닙니다! "div"와 같은 기본 HTML 태그의 경우 유형 필드가 문자열인 객체이거나 React 컴포넌트 인스턴스의 경우 함수인 객체입니다.
// React element for <div>oh my</div>
> React.createElement("div", { title: "oh my" })
{
$$typeof: Symbol(react.element),
type: "div",
props: { title: "oh my" },
...
}
// React element for <MyComponent>oh my</MyComponent>
> function MyComponent({children}) {
return <div>{children}</div>;
}
> React.createElement(MyComponent, { children: "oh my" });
{
$$typeof: Symbol(react.element),
type: MyComponent // reference to the MyComponent function
props: { children: "oh my" },
...
}
React에서 컴포넌트를 직렬화하는 과정은 컴포넌트의 유형에 따라 달라집니다.
따라서, React에서 기본 HTML 태그와 클라이언트 컴포넌트는 그대로 JSON으로 직접적인 방법으로 직련화할 수 있으며, 서버 컴포넌트 별도의처리(함수 호출 밑 결과짓랼학) 를 거치면서 최종적으론 모두 기본 HTML 태그 형태루 변환되어집니다.
RSC는 컴포넌트 함수 대신 직렬화 가능한 "참조"인 "모듈 참조"라고 하는 React 엘리먼트의 타입 필드에 가능한 새로운 값을 도입했습니다.
예를 들어 클라이언트 컴포넌트 엘리먼트는 다음과 같이 보일 수 있습니다.
{
$$typeof: Symbol(react.element),
// The type field now has a reference object,
// instead of the actual component function
type: {
$$typeof: Symbol(react.module.reference),
// ClientComponent is the default export...
name: "default",
// from this file!
filename: "./src/ClientComponent.client.js"
},
props: { children: "oh my" },
}
하지만 클라이언트 컴포넌트 함수에 대한 참조를 직렬화 가능한 '모듈 참조' 객체로 변환하는 이 손재주는 어디에서 발생하고 있을까요?
해결방안은 바로 번들러입니다!
RSC는 컴포넌트 함수 대신 직렬화 가능한 "참조"인 "모듈 참조"라는 새로운 값을 React 엘리먼트의 'type' 필드에 도입했습니다. 이를 통해 클라이언트 컴포넌트의 함수가 아닌, 해당 파일 이름과 내보내기 이름이 포함된 모듈 참조 객체를 사용할 수 있게 되었습니다.
예를 들어, 클라이언트 컴포넌트 엘리먼트는 'type' 필드에 실제 컴포넌트 함수 대신 모듈 참조 객체를 가지게 됩니다. 이 모듈 참조 객체는 해당 컴포넌트가 위치한 파일 이름과 내보내기 이름을 포함합니다.
이러한 변환 과정은 번들러에서 발생합니다. React 팀은 웹팩 로더 또는 노드-레지스터로 react-server-dom-webpack에서 웹팩에 대한 공식 RSC 지원을 제공합니다. 서버 컴포넌트가 *.client.jsx 파일에서 무언가를 가져올 때, 실제로 가져오는 것이 아니라 해당 파일 이름과 내보내기 이름만 포함된 모듈 참조 객체만 가져옵니다.
요약하면, RSC에서는 직렬화 가능한 '모듈 참조' 객체를 도입하여 클라이언트 컴포넌트의 함수 대신 사용하며, 이 변환 과정은 번들러에서 수행됩니다.
{
// The ClientComponent element placeholder with "module reference"
$$typeof: Symbol(react.element),
type: {
$$typeof: Symbol(react.module.reference),
name: "default",
filename: "./src/ClientComponent.client.js"
},
props: {
// children passed to ClientComponent, which was <ServerComponent />.
children: {
// ServerComponent gets directly rendered into html tags;
// notice that there's no reference at all to the
// ServerComponent - we're directly rendering the `span`.
$$typeof: Symbol(react.element),
type: "span",
props: {
children: "Hello from server land"
}
}
}
}
✨ React에서 컴포넌트를 직렬화하는 과정에서 기억해야할 부분
1. 기본 HTML 태그는 이미 JSON으로 직렬화할 수 있는 문자열 형태입니다.
2. 사용자 정의 서버 컴포넌트는 함수로 표현되며, 이를 직접 JSON으로 직렬화하는 것은 불가능합니다. 이를 해결하기 위해 React는 해당 프로퍼티와 함께 서버 컴포넌트 함수를 호출하고 그 결과를 직렬화하여 기본 HTML 태그 형태로 변환합니다.
3. 사용자 정의 클라이언트 컴포넌트는 'type' 필드에서 실제 함수 대신 모듈 참조 객체를 가리켜서 별도의 처리 없이 JSON으로 직렬화할 수 있습니다.
4. 이 모듈 참조 객체는 번들러에 의해 생성되며, 해당 파일 이름과 내보내기 이름을 포함합니다.
5. 따라서 React에서는 기본 HTML 태그와 클라이언트 컴포넌트는 그대로 JSON으로 직접적인 방법으로 직련화할 수 있으며, 서버 컴포넌트는 별도의 처리(함수 호출 밑 결과짓랼학) 를 거치면서 모두 기본 HTML 태그 형태로 변환됩니다.
이 프로세스가 끝나면 서버에서 다음과 같이 보이는 React 트리가 브라우저로 전송되어 'finisg up'되기를 바랍니다.

전체 React 트리를 JSON으로 직렬화하기 때문에 클라이언트 컴포넌트나 기본 html 태그에 전달하는 모든 prop도 직렬화가 가능해야 합니다. 즉, 서버 컴포넌트에서는 이벤트 핸들러를 프로퍼티로 전달할 수 없습니다!
// 현재 기준으로는 (2023.10) 잘못된 내용입니다.
// NOT OK: 서버 컴포넌트는 함수를 프로퍼티로 전달할 수 없습니다.
// 함수는 직렬화할 수 없으므로 서버 컴포넌트는 함수를 자손에게 전달할 수 없습니다.
function SomeServerComponent() {
return <button onClick={() => alert('OHHAI')}>Click me!</button>
}
-> 이 부분은 이 블로그를 보다가 발견한 사항입니다.
현재 Next.js 13에서는 Next의 서버 액션을 사용하여 React Server Component(RSC)에서 React Client Component(RCC)로 함수를 전달할 수 있습니다. 하지만 이 기능에는 중요한 제약사항이 있습니다. 전달되는 함수의 매개변수와 반환 값은 모두 직렬화 가능해야 합니다.
이전에 서버 측에서는 직렬화할 수 없는 데이터, 예를 들어 함수나 이벤트 핸들러 등을 처리하지 못한다고 한 내용은 완전히 정확하지 않을 수 있습니다. 사실상, 해당 데이터가 직렬화 가능하다면 서버 측에서도 처리가 가능합니다.
그러나 주의해야 할 점은, 모든 함수나 이벤트 핸들러가 직렬화 가능한 것은 아니라는 점입니다. 따라서 이 기능을 사용할 때에는 해당 요구사항을 충족하는지 확인해야 합니다.
하지만 여기서 한 가지 주의해야 할 점은 RSC 프로세스 중에 클라이언트 컴포넌트를 만나면 클라이언트 컴포넌트 함수를 호출하거나 클라이언트 컴포넌트로 "하강"하지 않는다는 점입니다.(RSC는 클라이언트 컴포넌트를 직접적으로 처리하지 않습니다.) 즉, RSC 프로세스가 실행되는 동안에 클라이언트 컴포넌트에 "도달"하면, 그 클라이언트 컴포넌트의 함수를 호출하거나 그 안으로 들어가지 않습니다.
예를 들어, 한 클라이언트 컴포넌트에서 다른 클라이언트 컴포넌트를 생성(인스턴스화)하는 경우가 있을 수 있습니다. 이런 상황에서도 RSC는 이 두 번째 클라이언트 컴포넌트로 "내려가서" 처리하지 않습니다.
다시 말해, 서버 쪽에서는 서버 컴포넌트만 처리하고, 실제로 실행되거나 렌더링 되는 클라이언트 컴포넌드의 내부 로직까지는 관여하지 않는다는 것입니다. 이런 방식으로 서버와 클라이언트 사이의 역할 분리를 명확하게 하고 있습니다.
function SomeServerComponent() {
return <ClientComponent1>Hello world!</ClientComponent1>;
}
function ClientComponent1({children}) {
// 클라이언트에서 프로퍼티로 함수를 전달해도 괜찮습니다.
// client components
return <ClientComponent2 onChange={...}>{children}</ClientComponent2>;
}
이 코드는 서버 컴포넌트에서 클라이언트 컴포넌트를 사용하고, 그 클라이언트 컴포넌트 내에서 또 다른 클라이언트 컴포넌트를 사용하는 예시입니다.
SomeServerComponent라는 서버 컴포넌트에서 ClientComponent1을 반환합니다. 그리고 ClientComponent1은 또 다른 클라이언트 컴포넌트인 ClientComponent2를 반환하며, 이때 이벤트 핸들러를 프로퍼티로 전달합니다.
하지만 React Server Component(RSC)의 동작 방식에 따르면, 서버에서 생성된 JSON 트리에는 ClientComponent2가 직접적으로 나타나지 않습니다. 대신, 모듈 참조와 ClientComponent1의 프로퍼티만 표시됩니다.
따라서 ClientComponent1이 이벤트 핸들러를 ClientComponent2에 프로퍼티로 전달하는 것은 문제가 없습니다. 왜냐하면 이 모든 처리는 클라이언트 쪽에서 발생하기 때문입니다. 서버 쪽에서는 함수나 이벤트 핸들러와 같은 직렬화할 수 없는 데이터를 처리하지 않기 때문입니다.
서버에서 받은 JSON 출력을 브라우저에서는 어떻게 처리하는 걸까요?
서버에서 보낸 JSON 출력을 브라우저가 받으면, 이를 사용해 React 트리를 재구성(렌더링) 시작합니다. 이 과정에서 "모듈 참조" 타입의 엘리먼트를 만나면, 해당 엘리먼트를 실제 클라이언트 컴포넌트 함수로 대체합니다.
서버 쪽에서는 번들러가 클라이언트 컴포넌트 함수를 모듈 참조로 대체했습니다. 반대로, 브라우저 쪽에서는 번들러가 모듈 참조를 다시 실제 클라이언트 컴포넌트 함수로 대체합니다.

우선 네 Suspense에서도 작동합니다.
Suspense는 React에서 데이터를 비동기적으로 로드하거나 느린 컴포넌트를 가져올 때 사용되는 기능입니다. Suspense를 사용하면 아직 준비되지 않은 요소가 필요할 때, React 컴포넌트에서 프로미스를 던질 수 있습니다. 이 프로미스는 "Suspense 경계"에서 처리됩니다.
서버에서 서버 컴포넌트 함수를 호출하여 RSC(React Server Component) 출력을 생성할 때, 해당 함수는 필요한 데이터를 가져오기 위해 프로미스를 던질 수 있습니다. 이렇게 발생한 프로미스가 해결될 때까지 해당 서버 컴포넌트의 렌더링을 일시 중지합니다.
브라우저 측면에서도 비슷한 과정이 일어납니다. 브라우저가 서버로부터 RSC JSON 출력을 받아들일 때, 서버가 처리하지 못한 프로미스(즉, 플레이스홀더)를 만나면 그 부분의 처리도 일시 중지합니다. 그리고 필요한 클라이언트 컴포넌트가 아직 로드되지 않았다면, 번들러 런타임이 해당 chunk를 동적으로 가져오게 됩니다.
따라서 Suspense는 서버와 클라이언트 사이에서 비동기 작업을 조율하는데 중요한 역할을 합니다. 이 기능 덕분에 서버 컴포넌트가 데이터를 스트리밍 방식으로 전송하고, 브라우저가 점진적으로 렌더링하며 필요에 따라 클라이언트 컴포넌트 번들을 동적으로 로드하는 것이 가능해집니다.
서버는 "스트리밍" 방식으로 데이터를 전송하며, 이 데이터는 "JSON" 형식입니다. 즉, 서버에서 클라이언트로 보내지는 각 데이터 조각은 JSON 블록이며, 각각의 블록은 고유한 ID로 태그가 지정됩니다.
예를 들어 OuterServerComponent 예제에 대한 React Server Component(RSC) 출력을 받아오는 경우, 이 출력 내용도 JSON 형태의 스트림으로 전달되게 됩니다.
따라서 클라이언트(브라우저)에서는 이렇게 받아온 JSON 스트림을 분석하고 처리하여 원하는 React 컴포넌트 구조를 재구성하게 됩니다.
아래 예제를 보겠습니다.
M1:{"id":"./src/ClientComponent.client.js","chunks":["client1"],"name":""}
J0:["$","@1",null,{"children":["$","span",null,{"children":"Hello from server land"}]}]
서버에서 보내는 JSON 스트림의 각 줄은 특정 정보를 나타냅니다.
'M'으로 시작하는 줄은 클라이언트 컴포넌트 모듈 참조를 정의합니다. 이 정보는 클라이언트 번들에서 특정 컴포넌트 함수를 찾아낼 때 필요합니다.
'J'로 시작하는 줄은 실제 React 엘리먼트 트리를 정의하며, '@1'과 같은 항목은 'M' 줄에 정의된 클라이언트 컴포넌트를 참조합니다.
이러한 형식을 사용하면 데이터 스트리밍이 매우 용이해집니다.
즉, 클라이언트가 각 행을 읽자마자 해당 JSON 스니펫을 파싱하고 처리할 수 있습니다. 서버가 렌더링 중일 때 Suspense 경계가 발생하면, 그에 해당하는 여러 개의 'J' 라인들을 볼 수 있습니다.
// Tweets.server.js
import { fetch } from 'react-fetch' // React's Suspense-aware fetch()
import Tweet from './Tweet.client'
export default function Tweets() {
const tweets = fetch(`/tweets`).json()
return (
<ul>
{tweets.slice(0, 2).map((tweet) => (
<li>
<Tweet tweet={tweet} />
</li>
))}
</ul>
)
}
// Tweet.client.js
export default function Tweet({ tweet }) {
return <div onClick={() => alert(`Written by ${tweet.username}`)}>{tweet.body}</div>
}
// OuterServerComponent.server.js
export default function OuterServerComponent() {
return (
<ClientComponent>
<ServerComponent />
<Suspense fallback={'Loading tweets...'}>
<Tweets />
</Suspense>
</ClientComponent>
)
}
M1:{"id":"./src/ClientComponent.client.js","chunks":["client1"],"name":""}
S2:"react.suspense"
J0:["$","@1",null,{"children":[["$","span",null,{"children":"Hello from server land"}],["$","$2",null,{"fallback":"Loading tweets...","children":"@3"}]]}]
M4:{"id":"./src/Tweet.client.js","chunks":["client8"],"name":""}
J3:["$","ul",null,{"children":[["$","li",null,{"children":["$","@4",null,{"tweet":{...}}}]}],["$","li",null,{"children":["$","@4",null,{"tweet":{...}}}]}]]}]
이 코드에서는 트윗 데이터를 가져오는 서버 컴포넌트 Tweets.server.js와 이 데이터를 렌더링하는 클라이언트 컴포넌트 Tweet.client.js가 사용됩니다.
JSON 스트림에서 'S2' 항목은 Suspense 경계를 나타냅니다. Suspense는 데이터가 아직 준비되지 않았을 때 어떻게 처리할지 정의합니다. 'J0' 항목의 자식 요소 중 '@3'은 아직 정의되지 않았습니다. 이는 트윗 데이터 로딩이 아직 완료되지 않아, 일단 placeholder로 사용하고 있기 때문입니다.
데이터 로딩이 완료되면 '@3'은 'J3'으로 대체됩니다. 그리고 'J3'은 참조하고 있던 'M4'(클라이언트 컴포넌트)에 데이터를 넘겨주어 화면에 보여집니다.
결국, React Server Component(RSC)와 React.Suspense를 함께 사용하면, 모든 데이터가 준비될 때까지 기다리지 않고 먼저 그릴 수 있는 부분부터 화면에 로드합니다. 그리고 나머지 필요한 데이터가 준비되면 즉시 업데이트 됩니다. 이렇게 하여 사용자 경험이 개선됩니다.
클라이언트에서 React 엘리먼트 트리를 재구성하는 것이 목표이기 때문입니다. HTML에서 React 엘리먼트를 생성하기 위해 HTML을 파싱하는 것보다 이 포맷에서 이 작업을 수행하는 것이 훨씬 쉽습니다. React 엘리먼트 트리의 재구성은 중요한데, 이를 통해 React 트리의 후속 변경사항을 최소한의 DOM 커밋으로 병합할 수 있기 때문입니다.
서버에서 API 요청을 통해 데이터만 가져와 클라이언트에서 렌더링하는 것과 비교하여, React Server Component(RSC)를 사용하는 것이 더 나은 선택인지는 화면에 렌더링할 내용에 따라 다릅니다.
RSC를 사용하면, '비정규화된' 데이터를 직접 얻을 수 있습니다. 이는 실제로 사용자에게 보여질 내용과 직접 연결되어 있습니다. 이런 방식은 특히, 가져온 데이터 중 일부만 화면에 보여주거나, 많은 양의 자바스크립트를 다운로드하지 않으려는 경우에 유리합니다.
또한 여러 데이터가 서로 의존적인 상황에서, 즉 하나의 데이터가 로드된 후 그 결과를 바탕으로 다른 데이터를 로드해야 하는 '폭포수' 형태의 상황에서도 RSC가 유리합니다. 이런 경우 서버에서 모든 필요한 데이터를 한 번에 가져오는 것이, 각각의 지연 시간을 겪으면서 클라이언트에서 여러 번에 걸쳐 데이터를 로드하는 것보다 효율적일 수 있습니다
React Server Component(RSC)는 서버에서 데이터를 가져와 클라이언트에 전달하는 역할을 합니다. 이를 통해 렌더링에 필요한 데이터를 더 효율적으로 처리할 수 있습니다.
RSC는 '비정규화된' 데이터, 즉 사용자에게 직접 보여질 내용과 연결된 데이터를 제공합니다. 이로 인해 필요한 부분만 화면에 로드하고, 많은 양의 자바스크립트 다운로드 없이도 렌더링을 처리할 수 있습니다.
또한 RSC는 여러 데이터가 서로 의존적인 상황에서 유리합니다. 하나의 데이터 로딩 후 그 결과를 바탕으로 다른 데이터를 로딩해야 하는 경우, 서버에서 한 번에 모든 필요한 정보를 가져오는 것이 각각의 지연 시간을 겪으면서 클라이언트에서 여러 번에 걸쳐 정보를 로드하는 것보다 효율적입니다.
그러나 RSC가 항상 최선의 선택은 아닙니다. 화면에 렌더링할 내용과 해당 작업을 수행하는 방법 등 여러 요소들을 고려하여 RSC 사용이 최적의 해결책인지 결정해야 합니다.
결론적으로, React Server Component(RSC)는 웹 애플리케이션의 성능 최적화와 사용자 경험 개선을 위한 중요한 도구일 수 있으며, 그 적용 가능성은 개별 프로젝트의 요구 사항과 목표에 따라 달라집니다.
React Server Components(RSC)와 Server-Side Rendering(SSR)은 서로 다른 개념입니다.
SSR은 클라이언트에게 보내기 전에 서버에서 페이지를 렌더링하는 방식을 의미합니다. 이렇게 하면 초기 로드 시간이 단축되고, 검색 엔진 최적화(SEO)가 향상됩니다. SSR은 전체 페이지를 렌더링하며, 결과적으로 HTML 문자열을 클라이언트로 보냅니다.
반면에 RSC는 서버에서 실행되는 React 컴포넌트를 의미합니다. 이 컴포넌트들은 HTML 대신 데이터의 일부분만을 클라이언트로 보낼 수 있습니다. RSC는 서버와 클라이언트 사이에서 부분적으로 렌더링하고, 필요한 데이터만 가져올 수 있어서 네트워크 비용과 시간을 절약할 수 있습니다.
따라서 RSC와 SSR은 모두 웹 애플리케이션의 성능을 향상시키는 방법들이지만, 사용 방식과 목표가 다릅니다