클라이언트까지 꼭 데이터가 와야할까? RCS(React Server Components)에 대해 알아보자
React 18에 등장한 React Server Components
React 18에서 새롭게 도입된 개념인 React Server Component(RSC)
에 대해 살펴봅시다.
React Server Components(RSC)란?
RSC는 서버에서만 렌더링되어 전달되는 컴포넌트를 의미합니다.
이는 HTML이 아닌 특별한 형태로 서버에서 렌더링되어 클라이언트에게 전달됩니다.
- 특히,
NextJS App Router
는 이를 기본적으로 지원합니다.
기존 방식의 문제점
컴포넌트 구조는 다음과 같이 설정됩니다
const App = () => {
return (
<Wrapper>
<ComponentA />
<ComponentB />
</Wrapper>
)
}
기존의 방식은 각 컴포넌트에서 API 호출을 통해 데이터를 렌더링합니다.
이렇게 하면 각 컴포넌트는 자신에게 필요한 데이터만을 받게 됩니다.
각각의 컴포넌트는 아래와 같은 형식으로 구성되어있습니다.
const Component = () => {
const [data, setData] = useState(null);
useEffect(() => {
// API 호출 등의 로직으로 dataB를 설정
// setData(responseData);
}, []);
return (
<div className="component">
{data ? <h1>{data.title}</h1> : "데이터를 불러오는 중..."}
</div>
);
}
이럴 경우 아래와 같은 문제점이 발생합니다.
- 여러 컴포넌트가 동시에 API를 호출하면 불필요한 네트워크 요청이 발생
- 일부 컴포넌트에서 데이터를 가져오는 데 시간이 오래 걸릴 경우 전체 렌더링 성능의 저하 발생
서버 컴포넌트로 어떻게 해결할 수 있을까?
RSC는 서버에서 렌더링되어 전체 페이지나 특정 부분에 필요한 데이터를 한 번에 가져올 수 있습니다.
조금 더 쉽게 말해보자면 페이지의 가장 상단에서 데이터들을 호출하고 하위 컴포넌트들에 props로 전달합니다.
효과
- 클라이언트에서의 불필요한 네트워크 요청 감소
-> API 호출 시 발생하는 네트워크비용, 렌더링 지연 감소 - 전체 애플리케이션의 퍼포먼스 향상
// ServerComponent.server.js
import { db } from './database'; // 예시로 데이터베이스 연결을 가정합니다.
function ServerComponent() {
// 서버에서 필요한 데이터를 직접 요청합니다.
const dataA = db.getDataForComponentA();
const dataB = db.getDataForComponentB();
return (
<Wrapper>
<ComponentA data={dataA} />
<ComponentB data={dataB} />
</Wrapper>
);
}
// Wrapper 컴포넌트는 동일하게 유지됩니다.
const Wrapper = ({ children }) => {
return (
<div className="wrapper">
{children}
</div>
);
}
// ComponentA
const ComponentA = ({ data }) => {
return (
<div className="componentA">
<h1>{data.title}</h1>
</div>
);
}
// ComponentB
const ComponentB = ({ data }) => {
return (
<div className="componentB">
<h1>{data.title}</h1>
</div>
);
}
서버 컴포넌트 사용 시 주의사항
서버 컴포넌트에도 주의사항이 있습니다. 어떤 점들을 유의해야 할까요?
- 상태 관리를 위한 로직 분리
- 서버 컴포넌트는 클라이언트의 상태와 직접적인 상호작용을 할 수 없기 때문에, 상태를 필요로 하는 컴포넌트는 클라이언트 컴포넌트로 처리해야 합니다.
// Example.server.js (서버 컴포넌트)
import fetch from 'node-fetch';
function ServerComponent() {
const data = fetch('https://api.example.com/data').then(res => res.json());
return <div>Data: {data}</div>;
}
// Example.client.js (클라이언트 컴포넌트)
import React, { useState } from 'react';
function ClientComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Click me!</button>
<p>You clicked {count} times</p>
</div>
);
}
- 서버 컴포넌트와 클라이언트 컴포넌트 사이의 데이터 전달
- 서버 컴포넌트로부터 데이터를 가져와 클라이언트 컴포넌트에게 props로 전달해야 합니다.
// App.server.js (서버 컴포넌트에서 데이터 가져오기)
import fetch from 'node-fetch';
import ClientComponent from './Example.client.js';
function App() {
const data = fetch('https://api.example.com/data').then(res => res.json());
return <ClientComponent serverData={data} />;
}
// Example.client.js (클라이언트 컴포넌트에서 데이터 사용하기)
import React from 'react';
function ClientComponent({ serverData }) {
return (
<div>
<h1>Data from Server: {serverData}</h1>
</div>
);
}
위의 예시는 간단한 형태로 RSC
를 통한 데이터 전달 및 클라이언트에서의 상태 관리를 보여주는 예시 코드입니다.
실제 구현 시에는 데이터 처리, 에러 핸들링 등의 추가적인 작업을 유동적으로 수행해주세요.
+1 서버 컴포넌트의 장점: 번들 사이즈 감소
- 번들 사이즈 감소(
zero-bundle-size
)- 서버에서 렌더링되는 컴포넌트의 코드는 클라이언트로 전송되지 않으므로 최종 번들 사이즈를 줄일 수 있습니다.
- 서버 컴포넌트가 클라이언트 번들에 포함되지 않는다는 것으로 사실상 번들의 크기가 0이라고 볼 수 있습니다. 따라서 RSC는
zero-bundle-size React Server Components
라고도 불립니다.
+2 서버 컴포넌트의 장점: 자동 코드 분할
서버 컴포넌트와 자동 코드 분할을 함께 사용하면 더욱 효과적입니다.
React Server Components는 필요한 컴포넌트만을 동적으로 불러와 퍼포먼스를 향상시킬 수 있습니다.
React에서 전통적으로 사용되는 클라이언트 컴포넌트의 코드 분할과 서버 컴포넌트를 이용한 코드 분할의 차이점을 보면서 확인해보겠습니다.
1. 전통적인 클라이언트 컴포넌트의 코드 분할
- React의 클라이언트 사이드에서는
React.lazy()
와 함께import()
문법을 사용하여 컴포넌트를 동적으로 불러올 수 있습니다.
-> 즉,레이지 로딩(lazy loading)
을 사용하는 방법입니다.
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
export default App;
위의 코드에서 LazyComponent
는 실제로 필요할 때만(렌더링 시점에) 동적으로 불러와지며, 그 전까지는 불러오지 않습니다.
2. React Server Components
를 사용한 자동 코드 분할
반면에 서버 컴포넌트를 사용하면, 클라이언트에서 불러오는 번들에 포함되지 않는 컴포넌트나 라이브러리를 사용할 수 있습니다.
// MyServerComponent.server.js
import ExpensiveLibrary from 'expensive-library'; // ZERO IMPACT on client bundle size
function MyServerComponent() {
const result = ExpensiveLibrary.someFunction();
return <div>{result}</div>;
}
export default MyServerComponent;
위의 코드에서 ExpensiveLibrary
는 클라이언트의 번들에 포함되지 않습니다.
따라서 클라이언트 측에서의 번들 사이즈는 이 라이브러리의 크기만큼 감소합니다.
이처럼 서버 컴포넌트는 무거운 라이브러리나 코드를 클라이언트 번들에서 제외할 수 있어 초기 로딩 시간을 크게 줄일 수 있습니다.
서버 컴포넌트와 클라이언트 컴포넌트의 적절한 조합은 React 애플리케이션의 성능과 사용자 경험을 크게 향상시킬 수 있습니다.
NextJS의 Server Components와 함께 사용하여 그 효과를 극대화시켜보아요!
References
reference1
reference2
reference3
reference4
reference5
reference6
reference7