

서버는 빈 HTML(<div id="root">)과 자바스크립트 번들(<script>)을 보내고, 브라우저는 자바스크립트 번들을 다운로드 및 파싱한 후 React 앱을 실행한다.
따라서 자바스크립트 번들이 로딩되기 전까진 빈 화면이 나타나며, 번들 크기가 커질수록 초기 로딩 시간이 길어지는 문제점이 있다.
이에 대해선 React.lazy, Suspense, Code Splitting 등으로 초기 로딩 부담을 줄일 수 있다. 처음에 전체 번들을 다 로딩하지 않고, 필요할 때 JS를 다운로드를 하고 그 동안 미리 지정한 UI를 보여줄 수 있다.
CSR의 단점을 개선하고자 서버 사이드 렌더링이 등장했다.

서버에서 React 컴포넌트를 실행하여 채워진 HTML을 보내어 사용자는 빈 화면이 아닌 완성된 HTML을 볼 수 있다. 이후 자바스크립트 번들을 로딩하여 hydration을 진행하면, 사용자와의 상호작용이 가능해진다.
사용자는 빈 화면 대신 빠르게 콘텐츠를 볼 수 있지만, JS 번들이 모두 다운로드되기 전까진 hydration을 진행하지 않기 때문에 인터랙션이 불가하다는 문제가 있다.
React 18에서 Streaming SSR과 Selective Hydration이 새롭게 도입되었다.
React의 내장 함수 renderToNodeStream을 사용하여 HTML을 조각 단위로 스트리밍할 수 있게 되었다. 즉 전체 HTML을 만들고 보내는 것이 아니라(renderToString), 준비된 HTML을 순차적으로 보낸다. 따라서 사용자는 부분적으로 완성된 HTML을 미리 볼 수 있다.
또한 선택적 하이드레이션(Selective Hydration)을 통해 우선순위가 높은 컴포넌트에 대해 먼저 hydration을 적용하여 사용자의 상호작용 시점을 개선할 수 있다.
Streaming SSR에서도 Suspense을 사용하여 아직 준비되지 않는 부분에 대해 fallback UI를 보여줄 수 있다.
💡 CSR(Code Splitting)와 Streaming SSR에서의 Suspense 역할 차이
1. CSR (Code Splitting + Suspense)
사용자가 어떤 컴포넌트를 렌더링하려는 런타임 시점(클라이언트에서 모듈을 import하는 시점)에 해당 컴포넌트에 대한 JS 번들을 서버에 요청한다. JS 번들을 로딩하는 동안 Suspense fallback을 표시한다.2. Streaming SSR + Suspense
사용자가 페이지를 열 때 서버에 페이지 전체의 HTML을 요청하고, 순차적으로 HTML 조각을 스트리밍 전송한다. 특정 컴포넌트가 전송되기 전까지 Suspense fallback을 표시한다.
하지만 SSR도, Streaming SSR도 hydration을 위해서는 자바스크립트 번들을 다운로드 받아야 한다. (JS에 대한 의존성은 그대로!)
React는 18버전에서 RSC의 개념을 제안했다.
서버 컴포넌트는 오직 서버에서만 실행되는 컴포넌트이다.
해당 코드는 클라이언트로 전송되지 않아서 번들 크기를 줄여 초기 로딩 속도를 개선하고, 서버 자원(DB, 파일 시스템, 내부 API)을 직접 활용할 수 있다.
서버 컴포넌트는 빌드 시 CI 서버에서 한 번 실행되거나(static) 또는 요청을 받을 때마다 웹 서버를 통해 실행된다(dynamic).
리액트 rsc 관련 문서에서는 서버 컴포넌트를 도입하게 된 동기를 다음과 같이 말했다.
The fundamental challenge was that React apps were client-centric and weren’t taking sufficient advantage of the server.
기본적으로 좋은 성능을 낼 수 있고 데이터 페칭을 쉽게 할 수 있는 다양한 솔루션을 내보았지만, 가장 근본적인 문제는 서버의 이점을 충분히 누리지 않는 클라이언트 중심적인 리액트 앱이었다는 것이다.
React.lazy를 사용해야 하고, 클라이언트 렌더링 시에 로딩이 이루어져 지연이 발생한다.useEffect 기반 데이터 페칭 방식은 waterfall 문제를 만든다. (parent 로딩 완료 전까지 child는 로딩 시작 X → 느린 속도)RSC를 사용하는 페이지는 항상 서버 요청에서 시작하며, 루트 컴포넌트는 서버 컴포넌트이다. 서버는 요청에 담긴 정보를 바탕으로 어떤 서버 컴포넌트를 사용할지, 어떤 props를 전달할지 결정한다.
서버는 루트 서버 컴포넌트를 직렬화(serialize)하여 브라우저로 보낸다.
html로 보내는 것보다 직렬화된 형식으로 보내는 것이 리액트 엘리먼트를 재구성하기에 유리하다.
리액트 엘리먼트는 결국 객체이기 때문에 직렬화할 수 있다.
이렇게 기본 html 태그로 이루어진 경우 다음과 같이 type 필드가 문자열이므로 직렬화가 가능하다.
// <div>oh my</div>
{
$$typeof: Symbol(react.element),
type: "div",
props: { title: "oh my" },
...
}
하지만 서버/클라이언트 컴포넌트의 경우 type 필드 값이 컴포넌트 함수의 참조가 되는데, 함수는 직렬화할 수 없기 때문에 다른 방식으로 처리되게 된다.
function MyComponent({children}) {
return <div>{children}</div>;
}
// <MyComponent>oh my</MyComponent>
{
$$typeof: Symbol(react.element),
type: MyComponent // reference to the MyComponent function
props: { children: "oh my" },
...
}
1) 서버 컴포넌트
type에 있는 함수(컴포넌트)를 props와 함께 호출하여 그 결과를 직렬화한다. 즉 실제 HTML 태그들로 렌더링해버린다.
2) 클라이언트 컴포넌트
번들러(webpack)은 함수 참조를 파일 이름과 export 이름을 포함한 모듈 참조 객체로 변환한다.
{
$$typeof: Symbol(react.element),
type: {
$$typeof: Symbol(react.module.reference),
name: "default", // default export
filename: "./src/ClientComponent.js"
},
props: { children: "oh my" }
}
클라이언트 컴포넌트는 서버에서 호출되지 않고 참조 정보만 남기므로, placeholder라고 표현한다.
결과적으로 다음과 같이 하나의 트리(JSON)가 만들어진다.

서버는 이 직렬화한 트리(JSON)를 브라우저로 보낸다.
다음과 같이 한 줄에 하나의 JSON blob과 ID가 있는 형식이다.
M1:{"id":"./src/ClientComponent.client.js","chunks":["client1"],"name":""}
J0:["$","@1",null,{"children":["$","span",null,{"children":"Hello from server land"}]}]
브라우저는 한 줄씩 읽으며 JSON을 파싱해 점진적으로 UI를 갱신하게 된다.
M으로 시작하는 줄은 클라이언트 컴포넌트 모듈 참조를 정의하는데, 클라이언트 번들에서 해당 컴포넌트를 어떻게 찾아야하는지에 대한 정보를 제공한다.
J로 시작하는 줄은 실제 리액트 엘리먼트를 정의한다. @1은 M으로 정의된 클라이언트 컴포넌트에 대한 참조를 의미한다.
Next.js에선 RSC Payload라고 부른다.
브라우저는 렌더링을 위해 리액트 엘리먼트 트리로 복원한다.
서버에서 번들러가 클라이언트 컴포넌트 함수를 모듈 참조로 바꾸었듯, 브라우저에서 번들러는 모듈 참조를 실제 클라이언트 컴포넌트 함수로 되돌리는 역할을 한다.
이 과정을 통해 재구성된 리액트 트리는 기본 HTML 태그와 클라이언트 컴포넌트로 구성되고, 이후 렌더 & 커밋을 통해 DOM에 반영된다.

React 19에서 서버 컴포넌트가 공식화되었지만, 실제로는 Next.js 같은 프레임워크를 사용하는 것이 일반적이다.
물론 프레임워크를 사용하지 않고 직접 서버 로직을 구현해 볼 수도 있다: https://itnext.io/react-server-components-without-frameworks-7c61c1ce0561
별도의 지시어("use client")가 없는 컴포넌트는 기본적으로 서버 컴포넌트가 된다.
따라서 다음과 같이 꼭 클라이언트 컴포넌트로 구현해야하는 경우에만 지시어를 명시하면 되겠다.
https://saengmotmi.netlify.app/react/what-is-rsc/
https://react.dev/reference/rsc/server-components
https://nextjs.org/docs/app/getting-started/server-and-client-components
https://www.plasmic.app/blog/how-react-server-components-work#what-are-react-server-components