이러한 문제점 때문에 다시 SSG/SSR가 각광을 받고 있다. 이제는 서버에서 페이지를 생성한 후, 이 HTML과 JS를 클라이언트로 전달하는 것이다. 그러나 이 렌더링 기법도 여전히 문제가 있다.
위와 같은 문제점을 해결하기 위해 React 서버 컴포넌트가 등장하게 되었다.
코드가 실행되는 위치를 정할 수 있다.
즉, 어떤 컴포넌트는 서버에서, 어떤 컴포넌트는 클라이언트에서 실행되게 지정할 수 있다.
이에 따라 클라이언트에 보내는 JS 파일의 양(특히 무거운 라이브러리를 import 할 때)을 줄일 수 있다. 서버 컴포넌트는 이미 서버에서 모두 실행된 후 직렬화된 형태로 클라이언트에 전달되기 때문에 어떠한 번들도 필요하지 않다. 그래서 번들 크기를 획기적으로 줄일 수 있다.
서버 컴포넌트는 컴포넌트 내에서 직접 데이터를 가져올 수 있다.
📢이전 리액트 컴포넌트의 Data fetching 문제 : Client - Server Waterfall
🧐 클라이언트 컴포넌트에서 데이터를 가져오려면?
카카오페이 기술 블로그: React Server Component
1. 모든 데이터를 부모 컴포넌트에서 하나의 거대한 API로 호출해 자식에게 내려준다.
- API 요청수를 줄일 수 있지만, 부모와 자식 간 coupling, 유지보수 어려워짐
- 불필요한 정보 over-fetching
2. 컴포넌트에 필요한 API를 각 컴포넌트에서 호출한다.
- high latency를 가진 클라이언트부터의 서버 요청이 늘어남
- 부모 컴포넌트 렌더링 후 데이터를 가져오고, 이 과정이 끝나기 전까지 자식 컴포넌트의 렌더링 및 API 호출 지연
RSC의 경우 Data fetch가 완료되면 해당 데이터를 클라이언트로 스트리밍할 수 있다.
예를 들어, 서버 컴포넌트는 DB, 파일 시스템 같은 서버 사이드 데이터 소스에 직접 접근할 수 있다.
더 이상 라이브러리를 사용하거나 상태를 관리하기 위해 useEffect를 사용할 필요가 없으며, getServerSideProps로 많은 데이터를 가져온 다음 props를 내려 보내지 않아도 된다.
이런 특징 때문에 React에서 데이터를 불러오는 것이 훨씬 더 쉬워진다.
IF) 폼 제출처럼 사용자의 클라이언트 작업에 대한 응답으로 서버에서 데이터를 가져와야 한다면?
클라이언트가 서버로 데이터 전송 -> 서버는 데이터를 가져옴 -> 응답을 다시 클라이언트로 스트리밍
그러나 이는 React Server Component보다는 React Actions와 관련이 있다.
현재는 CSS in JS를 사용할 수 없다.
emotion, styled-components 등의 라이브러리를 사용할 수 없다. 대신 Tailwind CSS 등을 사용해야 한다.
서버 컴포넌트에서 React Context가 작동하지 않는다.
React Context는 클라이언트 컴포넌트에서만 접근할 수 있다. 따라서, 서버 컴포넌트 간에 props를 사용하지 않고 데이터를 공유하려면 일반적인 모듈을 사용해야 한다.
유연하기 때문에 복잡하다.
서버 사이드
1. 리액트는 서버 컴포넌트를 React Server Component Payload (RSC Payload - JSON)라고 불리는 데이터 형식으로 렌더링한다.
2. Next.js는 RSC Payload와 'use client' 지시어를 보고 서버에서 HTML 태그와 클라이언트 컴포넌트 placeholder 트리로 렌더링한다.
클라이언트 사이드
1. RSC Payload와 번들링된 JS 파일을 사용해 클라이언트 컴포넌트, 서버 컴포넌트를 조합하고, DOM을 업데이트한다.
2. 필요하다면 'use client'로 지시된 클라이언트 컴포넌트를 하이드레이트하고, 앱을 인터랙티브하게 만든다.
서버 컴포넌트는 클라이언트 상태를 유지하며 refetch될 수 있다.
서버 컴포넌트는 HTML이 아니라, RSC Payload로 컴포넌트를 전달하므로 필요한 경우 focus, input 같은 클라이언트 상태를 유지하면서 여러 번 데이터를 가져오고, 리렌더링해 전달 가능하다.
SSR의 경우 HTML로 전달되기 때문에 refetch가 필요한 경우 HTML 전체를 리렌더링해야 하고, 이에 따라 클라이언트 상태를 유지할 수 없다.
모든 서버 컴포넌트에서는 서버에 접근 가능하다.
그러나 SSR을 구현할 때 가장 top level의 페이지에서만 getServerSideProps(), getInitialProps()로 서버에 접근 가능하다. (Next.js를 사용하는 경우)
서버 컴포넌트의 코드(JS 코드)는 클라이언트로 전달되지 않는다.
따라서 전체 번들 사이즈는 SSR보다 확연히 작다.
그러나 서버 사이드 렌더링 페이지의 모든 컴포넌트 코드는 자바스크립트 번들에 포함되어 클라이언트로 전송된다. (하이드레이션이 필요할 때)
현재 RSC는 Next.js 13를 통해서 구현할 수 있다.
사실상 Next.js 13의 app directory에 작성되는 모든 컴포넌트는 서버 컴포넌트가 된다.
// 데이터 가져오기 및 스트리밍 기능이 있는 서버 컴포넌트
import { Suspense } from 'react'
async function VideoSidebar({ videoId }) {
return (
<Suspense fallback={<p>댓글 로딩 중...</p>}>
<Comments videoId={videoId} />
</Suspense>
<Suspense fallback={<p>관련 동영상 로딩 중...</p>}>
<RelatedVideos videoId={videoId} />
</Suspense>
)
}
클라이언트 컴포넌트 사용하기
'use client'를 추가하면 해당 모듈 뿐만 아니라 이곳에서 import하는 모든 컴포넌트 또한 클라이언트로 전송된다.
'use client';
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return <button onClick={increment}>The count is {count}</button>;
}
// video/page.client.tsx
/**
* Data fetching을 제외한 전체 앱이 여기에 있음
*/
'use client';
export default function App({data}) {
return (
<>
<Player videoId={data.videoId} />
<Title content={data.title} />
</>
);
}
// video/page.tsx
/**
* 서버에서 데이터를 가져와서, 그 데이터를 클라이언트 컴포넌트에 전달함
*/
import App from './page.client.jsx'
// 예전에는 getServerSideProps였음
async function fetchData() {
const res = await fetch('https://api.example.com')
return await res.json()
}
export default async function FetchData() {
const data = await fetchData()
{/* 페이지의 콘텐츠를 이 클라이언트 컴포넌트로 옮겼습니다. */}
const <App data={data} />
}
// video/Player.jsx
'use client';
import MuxPlayer from '@mux/mux-player-react';
function Player({ videoId }) {
return <MuxPlayer streamType="on-demand" playbackId={videoId} />;
}
```
```tsx
// video/Title.jsx
function Title({ content }) {
return <h1>{content}</h1>;
}
import ClientComponent from './ClientComponent.tsx';
import ServerComponentB from './ServerComponentB.tsx';
/**
* 1. 서버 컴포넌트를 클라이언트 컴포넌트의 children으로 전달한다.
*/
function ServerComponentA() {
return (
<ClientComponent>
<ServerComponentB />
</ClientComponent>
);
}
/**
* 2. 서버 컴포넌트를 클라이언트 컴포넌트의 prop으로 전달한다.
*/
function ServerPage() {
return <ClientComponent content={<ServerComponentB />} />;
}
2) 파일의 반은 서버 컴포넌트로, 반은 클라이언트 컴포넌트로 만들고 싶다면?
구문을 하이라이팅하는 <CodeBlock /> 컴포넌트가 있다고 하자.
코드가 여러 개 있고 사용자의 인터랙션에 따라 여러 코드 사이를 전환할 수 있다고 할 때,
구문 하이라이팅은 서버 컴포넌트로, 이동은 클라이언트 컴포넌트로 구현할 수 있다.
// components/CodeBlock/CodeBlock.server.js
import Highlight from 'expensive-library'
import ClientCodeBlock from './CodeBlock.client.js'
import { example0, example1, example2 } from './examples.js'
export default function ServerCodeBlock() {
return (
<ClientCodeBlock
// prop으로 전달하기 때문에 서버 전용으로 유지된다.
renderedExamples={[
<Highlight code={example0.code} language={example0.language} />,
<Highlight code={example1.code} language={example1.language} />,
<Highlight code={example2.code} language={example2.language} />
]}
/>
)
}
// components/CodeBlock/CodeBlock.client.js
'use client';
import { useState } from 'react';
export default function ClientCodeBlock({ renderedExamples }) {
// 상태 및 이벤트에 반응해야 하므로 클라이언트 컴포넌트여야 한다.
const [currentExample, setCurrentExample] = useState(1);
return (
<>
<button onClick={() => setCurrentExample(0)}>Example 1</button>
<button onClick={() => setCurrentExample(1)}>Example 2</button>
<button onClick={() => setCurrentExample(2)}>Example 3</button>
{renderedExamples[currentExample]}
</>
);
}
// components/CodeBlock/index.ts
export { default } from './CodeBlock.server.js';
50,000줄의 코드를 React 서버 컴포넌트로 옮기기 전에 알았더라면 좋았을 것들
Next.js 13 공식 문서 - Server Components
카카오페이 기술 블로그: React Server Component
요즘 IT: 새로 등장한 '리액트 서버 컴포넌트' 이해하기
yceffort: 리액트 서버 컴포넌트의 동작 방식
Toast UI: React 서버 컴포넌트
Vercel: Understanding React Server Components
Harry's diary: Next.js 13으로 알아보는 FE 렌더링 방식 (SSR vs RSC)