React Server Component
React의 server component에 대해 알아보자
개요
이번에 Next13을 쓰며 "use client"와 관련된 문제를 많이 겪었습니다. 이는 Next13으로 변하며 pages router에서 app router로 변하며 기본적인 모든 component가 React Server Component로 변했기 때문이었는데, 이를 기회삼아 React Server Component에 대해 알아보고자 합니다.
리액트 서버 컴포넌트(RSC)
RCS는 간단하게 말하면 서버에서 동작하는 컴포넌트를 가리킵니다. 서버에서 실행하고 렌더링 되는 컴포넌트로, 이 때 RCS는 서버에서 해석된 이 후 직렬화가 가능한 상태로 클라이언트에 전달됩니다. 또한 서버에 남아있고, 서버에서 렌더링 되기 때문에 RCS는 백엔드 리소스에 직접 접근이 가능합니다.
그렇다면 클라이언트 컴포넌트도 있을까요? 바로 이전에 우리가 기본적으로 쓰고 있던 컴포넌트가 클라이언트 컴포넌트(React Client Component, RCC) 입니다.

두 컴포넌트의 차이점을 간략하게 요약하면 위 사진과 같습니다.
위 표를 보며 알 수 있는 주된 차이점 중 하나는 바로 RSC에서는 사용할 수 없는 것들이
있는 점인데요. 대표적으로 React Hook과 이벤트 리스너등이 있습니다. 왜 이런 차이점이
생겼을까요? 이는 RSC의 동작 방식을 이해해야 합니다. 사용자가 페이지의 요청을 서버에
날리게 되면, 서버는 이때부터 컴포넌트 트리를 실행하여 root부터 직렬화된 json형태로
재구성 하기 시작합니다.
하지만 컴포넌트의 모든 객체가 직렬화가 가능한 것은 아닙니다. 대표적으로 함수(Function),날짜(Date)등의
객체는 직렬화 불가능한 데이터로 취급되므로 RSC에서 사용할 수 없습니다. 마찬 가지로
React Hooks또한 사용이 불가능 합니다.
그렇다면 직렬화 과정에서 RCC를 만나게 되면 어떻게 될까요? RCC는 그 자체로 하나의 함수이므로, 직렬화가 불가능 합니다. 따라서 직렬화 과정에서 RCC를 만나게 되면 해당하는 자리는 RCC가 들어갈 자리라는 것이 명시된 후 넘어가게 됩니다.
따라서, RCC와 RSC를 쓸 때, RCC 내부에 RSC를 사용하게 되면 무용지물이 되어버리게 됩니다. 직렬화 과정에서 RCC를 만나게 되면 아무리 내부에 존재하는 컴포넌트가 RSC라고 하더라도 직렬화가 불가능하기 때문에 RSC의 장점을 살릴 수 없게 됩니다.
RSC의 특징
지금까지 RSC를 간단하게 살펴보았습니다. 이런 RSC가 이끌어내는 특징은 무엇이 있을까요?
1. 클라이언트에 전달되지 않음
어디까지나 RSC는 서버에 존재하는 컴포넌트로, 클라이언트에 전달되지 않습니다. SSR과의 다른 점으로, SSR은 JS번들이 클라이언트에 전달되지만, RSC는 JS번들이 전달되지 않습니다. 따라서 아래와 같은 특징이 발생합니다.
2. 제로 번들 사이즈
RSC는 앞서 말했듯이 서버에서 실행된 후 직렬화된 JSON의 형태로 클라이언트에 전달됩니다. 때문에 어떠한 bundle도 필요하지 않습니다. 이는 기존 컴포넌트(RCC)는 브라우저에서 다운로드 되지만, RSC는 컴포넌트의 코드 및 번들이 서버에서 다운되고, 이 번들이 브라우저로 전송이 되지 않기 때문입니다.
때문에 RSC에서 사용하는 외부 라이브러리의 경우에도 번들에 포함되지 않으며, 이는 번들 사이즈를 감량하는데 큰 도움이 됩니다.
3. Code Splitting
위의 장점과 연계되는 장점입니다. 기존 Reat App에서는 번들 사이즈를 줄이기 위해 Code Splitting이라는 기법이 흔히 사용되었습니다.
예를 들어
import React from "react";
const OldPhotoRenderer = React.lazy(() => import("./OldPhotoRenderer.js"));
const NewPhotoRenderer = React.lazy(() => import("./NewPhotoRenderer.js"));
function Photo(props) {
if (FeatureFlags.useNewPhotoRenderer) {
return <NewPhotoRenderer {...props} />;
} else {
return <OldPhotoRenderer {...props} />;
}
}위와 같은 방식으로 React.lazy나 dynamic import를 사용하는 방식이 그 예시입니다. 필요한 컴포넌트를 동적으로 불러옴으로써 퍼포먼스를 높이려는 것이었죠.
다만 이러한 방식은 lazy loading이 필요한 컴포넌트마다 일일이 적용해야 하며, 부모 컴포넌트가 렌더링 된 이후에 로딩을 시작하기 때문에 화면에 보이기까지 딜레이가 존재하는 단점이 있었습니다.
하지만 RSC에서는 자연스럽게 이 문제가 해결됐습니다. RSC에서 import되는 모든 RCC는 code splitting 포인트로 간주되기 때문에 따로 명시하지 않아도 dynamic import가 되게 됩니다.
4. 서버 리소스 접근
RSC는 서버에서 동작하기 때문에 db, fs 등 서버 사이드 데이터에 접근할 수 있습니다.
import fs from 'react-fs';
import db from 'db.server';
function Note(props) {
const note = db.notes.get(props.id); // 데이터베이스 접근
const noteFromFile = JSON.parse(fs.readFile(`${id}.json`)); // 파일 접근
if (note == null) {
// ...
}
return (/* render... */);
}
이렇게 서버에서 가져온 데이터는 RCC에 전달 할 수 있습니다. 단, 직렬화 가능한 props만 가능합니다. 예를 들어, 함수는 불가능 합니다.
결론
결론적으로 RSC는 RCC의 진보된 버전이 아니라, RCC와 같이 써야하는 새로운 타입의 Component입니다. 앞서 나열한 특징 때문에 RSC는 주로
- Data Fetching
- 백엔드 자원 접근
- API Key, JWT 따위를 서버에서 유지하는 경우
- 등등...
에 사용되고, RCC는
- 상호작용, 이벤트 리스너가 필요한 경우
- React Hooks가 필요한 경우(state 등)
- browser api를 사용해야 하는 경우
- 등등...
에 사용되는 것입니다. 이런 특징을 잘 파악하고 RSC와 RCC를 적절히 배치하여 사용하는 것으로 어플리케이션의 성능을 더욱 끌어올릴 수 있을 것 입니다.