Development Study/Frontend

클라이언트까지 꼭 데이터가 와야할까? RCS(React Server Components)에 대해 알아보자

  • -
728x90

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>
    );
}

서버 컴포넌트 사용 시 주의사항

서버 컴포넌트에도 주의사항이 있습니다. 어떤 점들을 유의해야 할까요?

  1. 상태 관리를 위한 로직 분리
    • 서버 컴포넌트는 클라이언트의 상태와 직접적인 상호작용을 할 수 없기 때문에, 상태를 필요로 하는 컴포넌트는 클라이언트 컴포넌트로 처리해야 합니다.
// 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>
    );
}
  1. 서버 컴포넌트와 클라이언트 컴포넌트 사이의 데이터 전달
    • 서버 컴포넌트로부터 데이터를 가져와 클라이언트 컴포넌트에게 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

728x90
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.