React Server Component (RSC)
서버 컴포넌트는 말그대로 서버 단에서 실행되는 컴포넌트이다. 2020년 React 18 버전에서 처음 선보여 Next.js 14 앱 라우터의 기본 컴포넌트 렌더링 방식으로 자리 잡았다. 마실가실 프로젝트를 Next 프레임워크로 작업하면서 궁금한 점도 많고 헷갈리는 부분도 있었어서, 서버 컴포넌트의 작동 원리와 이점에 대해 자세히 살펴보고자 한다.
서버 컴포넌트의 렌더링 방식 : RSC Payload
서버 컴포넌트 형태로 작성된 코드는 리액트에서 React Server Component Payload (RSC Payload)라는 특수한 데이터 형식으로 클라이언트 단에 전달된다. Element Tree 형태의 압축된 바이너리 표현으로, 객체와 유사한 형태를 띄고 있었다. 이는 클라이언트 단에서 브라우저의 DOM을 조정(Reconcile) 및 갱신하는 데에 사용된다. (HTML 구문 형태로 렌더링되는 SSR과의 차이점을 여기서 확인할 수 있다) RSC Payload는 서버 컴포넌트의 렌더링 결과, 클라이언트 컴포넌트의 렌더링 위치 및 JS 파일에 대한 참조 Placeholder, 클라이언트 컴포넌트에게 전달해야하는 프롭들로 구성된다.
M1:{"id":"./src/ClientComponent.client.js","chunks":["client1"],"name":""}
J0:["$","@1",null,{"children":["$","span",null,{"children":"Hello from server land"}]}]
스트림이 정확히 어떻게 구성되어있고, 어떤 방식으로 작동하는지 궁금해서 더 찾아보았다. RSC Payload 스트림의 한 예를 가져왔는데, M으로 시작하는 행이 클라이언트 컴포넌트의 렌더링 위치를 명시한 곳이다. 이를 실제 Element Tree(서버 컴포넌트의 렌더링 결과)인 J가 @N Placeholder을 통해 참조하게 된다. 함수나 클래스 형태의 클라이언트 컴포넌트는 직렬화할 수 없기 때문에, 이와 같은 형태로 전달된다고 한다. (M과 J 말고도 Suspense, Promise 등 다양한 Placeholder가 존재한다!)
어떻게 실제 React 컴포넌트로 변환될까?
이 특수한 데이터 형식이 어떻게 변환되는지 찾아봤는데, react-server-dom-webpack가 RSC 응답을 Element Tree 형태로 변환하는 진입점을 담고 있다고 한다. 그래서 Next의 코드를 한 번 까봤다.
🔗next.js/packages/next/src/client/app-index.tsx
(Line7) import { createFromReadableStream } from 'react-server-dom-webpack/client'
(Line121) const initialServerResponse = createFromReadableStream(readable, {
callServer,
})
function ServerRoot(): React.ReactNode {
return use(initialServerResponse)
}
방대한 양의 코드들이라 정확하지 않을 수 있지만, 읽을 수 있는 형태의 스트림을 createFromReadableStream 함수를 통해 ReactNode로 반환하는 것을 확인할 수 있었다!
그럼 클라이언트 컴포넌트(RCC)는?
이야기가 좀 돌아왔는데, 정리하면 서버 컴포넌트로 작성된 코드는 서버로부터 특수한 데이터 형태로 클라이언트에 전달되고 웹팩 메서드를 통해 non-interactive한 상태로 화면에 그려지게 되는 것을 알 수 있었다. 클라이언트 컴포넌트도 여기까진 동일한 과정을 거친다. RCC 또한 SSR 방식으로 작동한다는 근거가 되는 부분이다. 이후 RSC Payload 스트림에서 placeholder로 표기해놨던 클라이언트 파일의 위치를 찾아 자바스크립트 번들을 다운로드하고, 구문을 분석하는 Hydration을 통해 interactive한 상태로 만들어주는 과정이 이루어진다. 또한 사용자의 추가적인 상호작용을 통해 후속 탐색(Subsequent Navigations)이 이루어진다면 온전히 클라이언트에서 리렌더링이 발생하게 된다.
서버 컴포넌트의 이점
여기까지 서버와 클라이언트 컴포넌트가 코드에서 시작해 화면에 헨더링되는 과정을 살펴보았다. 그럼 이 개념을 왜 사용하는 것이고, 기존의 어느 문제를 해결해줄 수 있을까?
서버 리소스 접근성 (Server Resources Accesibility)
서버에서 가져오는 데이터들을 서버 상에서 곧바로 fetching하기 때문에, 기존에 클라이언트로 데이터를 전달하는데 걸리는 시간과 요청 수를 감소시켜 성능을 향상시킬 수 있다. 심지어 DB나 파일 시스템에 직접 접근해 데이터를 fetching해올 수 있다...! Server Action에서 사용하는 것처럼 Zod나 Plasma를 이용해 데이터에 대한 갱신까지 함께 사용하면 시너지가 엄청날 것 같다. 단 직렬화 가능한 형태의 데이터만 가져올 수 있으며, 이벤트 핸들러나 함수는 전달할 수 없다.
function Note(props) {
const note = db.notes.get(props.id); // 데이터베이스 접근
const noteFromFile = JSON.parse(fs.readFile(`${id}.json`)); // 파일 접근
if (note == null) {
// handle missing note
}
return (/* render note here... */);
}
@React18 : 서버 컴포넌트 준비하기 - 자유로운 서버 리소스 접근
제로 번들 사이즈 컴포넌트 (Zero-bundle Sized Component)
기존에 클라이언트 컴포넌트의 렌더링을 위해서는 사용되는 패키지들이 번들에 포함된채로 클라이언트에 전달되며, 이 코드와 데이터의 의존성을 전부 다운로드받은 후에야 어플리케이션을 실행할 수 있었다. 그러나 서버 컴포넌트는 브라우저에 다운로드할 필요없이, 클라이언트에게 정적인 컨텐츠를 전달하기 때문에 번들 사이즈에 영향을 끼치지 않게 된다. 요즘 D3를 공부하고 있는데, 서버 컴포넌트가 효자 노릇을 톡톡히 할 것 같다.
자동 코드 분할 (Auto Code Splitting)
용량이 큰 JS 번들을 여러 개의 작은 번들로 쪼개어 필요 시에만 클라이언트로 전달하는 코드 스플리팅 방식을 React.lazy나 Dynamic Import를 통해 구현할 수 있었다. 이는 브라우저의 성능을 향상시킬 수 있지만, 일일이 적용해야 하는 수고로움이 있었고, 딜레이가 발생했었다. 서버 컴포넌트에서는 이를 해결하기 위해 모든 클라이언트 컴포넌트를 Code Splitting 포인트로 간주, 위의 두 문제를 해결할 수 있게 된다. 단 Suspense는 서버 측에서도 대기 시간을 관리하여 데이터의 왕복 시간을 단축해주기 때문에, 대체가 아닌 함께 사용해 시너지를 낼 수 있다고 볼 수 있을 것 같다.
서버 컴포넌트 vs SSR
위에서도 언급하였듯, 서버 컴포넌트는 SSR이 아니다. 두 방식 모두 서버에서 렌더링이 이루어진다는 점이 같아 혼동이 올 수 있지만 명확한 차이점이 존재한다.
렌더링 형식의 차이
SSR은 ReactDOM의 renderToString() 메서드를 통해 HTML 구문으로 컴포넌트를 렌더링한다. 이후의 페이지 로딩에서는 사용자의 상호작용에 따라 Hydration 과정을 거쳐 부분적인 리렌더링(CSR)이 이루어지게 된다. 이 Hydration이 이루어지면서 결국 번들이나 스크립트 파일을 전부 다운받게 된다는 점 또한 서버 컴포넌트와의 차이점이다.
반면 서버 컴포넌트는 직렬화 가능한, 객체 형태의 Element Tree로 구성되어 있으며, 페이지 레벨에 구애받지 않고 어디서든 서버에 접근할 수 있다. 또한 HTML 형태가 아니기 때문에, 상태값을 유지하며 서버로부터 데이터를 가져오고 리렌더링할 수 있다.
레퍼런스
🔗Server Component - Next.js 공식문서
🔗React18 : 리액트 서버 컴포넌트 준비하기 - 카카오페이 기술블로그
🔗How React Server Component Works - Plasmic Blog
🔗difference between RSC with SSR - Stack Overflow
🔗Why do Client Components get SSR'd to HTML? - Github reactwg discussions
본문 중 제가 잘못 알고 있는 부분, 궁금하신 부분이 있다면 언제든 코멘트 주세요! 긴 글 읽어주셔서 감사드립니다.