Lighthouse of FE biginner

[React] RSC (React Server Component) 본문

[WEB] 프론트엔드

[React] RSC (React Server Component)

[FE] Lighthouse 2024. 5. 22. 15:10

React Server Component

RSC가 서버에서 실행이 되지만 RSC 와 SSR 은 동일한 개념이 아닙니다.
 
RSC 는 클라이언트에서 실행되는 컴포넌트(RCC)와는 다르게, 빌드 시점에 생성이 되거나 서버와 함께 사용한다면 서버에서 랜더링이 수행되는 컴포넌트 입니다.
 

RSC의 개요

RSC는 서버에서 랜더링이 수행된다. 즉 클라이언트에서 랜더링이 수행되지 않기 때문에 클라이언트 전용 API를 사용할 수 없다. 또한 서버에서 랜더링이 되기 때문에 클라이언트에서 랜더링이 발생하지 않는다. (리랜더링이 없다.)
 
기본적으로 모든 컴포넌트는 서버 컴포넌트이다. 만약 클라이언트 컴포넌트를 사용하고 싶다면 "use client"를 컴포넌트 파일 최상단에 기입해야한다.
 
서버 컴포넌트를 가지고 있는 페이지는 요청을 받으면 서버에서 랜더링이 수행이 된다. 서버 컴포넌트는 랜더링을 수행해 JSON 노드로 파싱이 되고, 클라이언트 컴포넌트는 랜더링을 수행할 수 없기 때문에 자신이 클라이언트 컴포넌트라는 placeholder를 JSON 트리에 남겨 놓는다.
 
직렬화 된 JSON 트리는 네트워크를 통해 클라이언트에 스트리밍 된다. 클라이언트에서는 스트리밍 중인 데이터를 받아서 랜더링을 수행하기 때문에 레이턴시 없이 랜더링을 수행할 수 있다. 이때 스트리밍으로 랜더링 되는 방식은 React 18v에서 소개된 Suspense with Streaming SSR 방식으로 랜더링 된다. 그렇기 때문에 반드시 Suspense로 감싸서 Promise가 resolve 될 때 까지의 UI를 구현해줘야한다.
 

RSC 랜더링 과정

    1. 클라이언트에서 페이지 요청을 보낸다. 페이지가 서버 컴포넌트를 사용하고 있다면 서버에서 페이지에서 사용중인 컴포넌트들을 JSON 트리로 만드는 랜더링 과정이 수행 (직렬화 과정)된다.
    2. 서버 컴포넌트는 아래와 같은 모양의 JSON으로 직렬화 된다.
      // JSON으로 직렬화 된 RSC
      
      {
        $$typeof: Symbol(react.element),
        type: "div",
        props: { style:{backgroundColor:"green"}, children:"hello world" },
        ...
      }
    3. 클라이언트 컴포넌트는 서버에서 랜더링을 수행할 수 없기 때문에 placeholder 노드로써 위치한다. 이때 type으로 module.reference라는 특별한 타입을 넣어주고, 해당 컴포넌트 파일의 위치를 명시해 줌으로써 직렬화를 우회한다.
      // 직렬화 된 RCC
      {
        $$typeof: Symbol(react.element),
        type: {
          $$typeof: Symbol(react.module.reference),
          name: "default", //export default를 의미
          filename: "./src/ClientComponent.js" //파일 경로
        },
        props: { children: "some children" },
      }
       
    4. 직렬화가 된 (서버측에서 랜더링을 수행하고 JSON 트리를 파싱한) 데이터를 클라이언트에 스트리밍 한다.
    5. 클라이언트에서 스트리밍 중인 데이터를 받는다. 응답 받은 데이터를 파싱한 후 리액트 fiber 엔진이 랜더링 트리를 만든다. 랜더링 과정을 거친 후 DOM에 커밋하는 커밋 페이즈를 거치게된다.

 

RSC 에 생길 수 있는 오해

  1. 서버 컴포넌트는 컴포넌트 단위로 서버에서 랜더링이 되는 것이 아닌 요청한 페이지 단위로 서버에서 컴포넌트 랜더링을 수행한다.
  2. SSR (서버 사이드 랜더링) 과 RSC 가 동일하다고 생각이 들 수 있지만 전혀 다른 기술이다. SSR은 HTML을 서버에서 랜더링 해 브라우저에 내려주지만, RSC는 HTML을 랜더링 하는 것이 아닌 컴포넌트 트리를 랜더링 해 직렬화 한 후 클라이언트에 내려준다.

 

기존 RCC의 문제점과 RSC의 이점

  1. 클라이언트에서 네트워크 워터폴을 해결할 수 있다.
    1. CSR의 경우 클라이언트에서 JS를 내려받고 랜더링한 후 API를 호출해 데이터를 받아와야한다. 이 과정에서 중첩된 컴포넌트에서 모두 API 호출을 한다면 사용자는 최종 결과물인 페이지를 받아보기 까지 여러 API의 요청을 기다려야 한다는 Waterfall을 경험하게 된다.
    2. 서버 컴포넌트의 경우 서버에서 랜더링이 수행될 때 DB 자원이나 서버에 접근해 데이터를 가져온 후 랜더링이 완성된 컴포넌트를 내려주기 때문에 네트워크 waterfall 이 없다.
  2. 낮은 latency
    1. 서버에서 랜더링이 수행되기 때문에 클라이언트에서 에셋을 내려받고 랜더링하고의 과정을 거치지 않는다. 그렇기 때문에 RCC보다 상대적으로 적은 latency를 경험한다.
  3. Zero bundle size
    1. 클라이언트 컴포넌트는 클라이언트에서 컴포넌트를 랜더링 해야하기 때문에 작성된 컴포넌트 파일을 번들링하고 번들링된 파일을 chunk 형식으로 브라우저에 내려줘야한다. 서버 컴포넌트는 서버에서 랜더링을 수행하고 직렬화 된 데이터를 스트리밍으로 내려주기 때문에 브라우저에서 내려받는 데이터의 사이즈가 zero에 가깝다.

 

RSC 사용 시 주의점

  1. 클라이언트 api를 사용할 수 없다. 서버 환경(Node 환경) 에서 랜더링을 수행하기 때문에 window, document 같은 기본 브라우저 객체나 React의 클라이언트 API (hooks)를 사용할 수 없다.
  2. 클라이언트 컴포넌트는 서버 컴포넌트를 호출할 수 없다. 클라이언트에서 서버 컴포넌트를 랜더링 할 수 없기 때문이다. 하지만 서버 컴포넌트에서 클라이언트 컴포넌트를 호출하고 합성 컴포넌트 패턴을 사용해 클라이언트 컴포넌트의 자식 컴포넌트로 서버 컴포넌트를 랜더링 할 수는 있다.
    1. 서버 컴포넌트는 서버 컴포넌트를 호출할 수 있으며 서버 컴포넌트는 클라이언트 컴포넌트를 호출할 수 있다.
  3. 서버 컴포넌트는 Suspense 와 함께 사용해야 한다.
RSC, RCC를 사용한 아키텍처를 도식화 한 다이어그램

 

RSC 사용 방법

계층적으로 서버 컴포넌트는 클라이언트 컴포넌트 하위에 위치할 수 없습니다.

async function PostSection() {
  const contents = await fetch(url);
  return (<div>{contents.map((content) => <Post content={content}>)}</div>);
}

function Post(content) {
  return (
    <div>{content.title}</div>  
  );
}

 
클라이언트 컴포넌트에서 서버 컴포넌트를 import 해 사용할 수 없음. 브라우저에서는 서버 컴포넌트를 실행할 수 없기 때문입니다.

// server.jsx
export async function Server() {
  const data= await fetch(url).then((res) => res.json());
  return <div>server</div>
}

// 불가능
// client.jsx
function Client() {
  return <Server/>
}

 
서버 컴포넌트에서 클라이언트 컴포넌트의 자식으로 서버 컴포넌트를 내려주는건 가능합니다. 즉 합성 컴포넌트 패턴으로 사용하는 것은 가능합니다.

// server.jsx
export async function Server() {
  const data= await fetch(url).then((res) => res.json());
  return <div>server</div>
}

// client.jsx
export function Client({children}) {
  return <div>{children}<div/>
}

// Server2.jsx
export async function ServerTwo() {
  return (
    <Client>
      <Server/>
    </Client>
  );
}

 

React 19 use API

 
React 19버전의 use 훅을 사용한다면 클라이언트 컴포넌트에서도 Server 컴포넌트에서 내려주는 Promise를 기다릴 수 있습니다. use 훅은 Promise가 resolve 될 때 까지 랜더링을 멈추게 합니다. use 훅을 사용할 땐 Suspense를 활용해 사용자에게 선언적으로 fallback UI를 보여줄 수 있습니다.

export async function Server() {
  const data= await fetch(url).then((res) => res.json());
  return (
    <Suspense fallback={"Loading.."}>
      <Client data={data} />
    </Suspense>
  );
}

// client.jsx
export function Client(data) {
  const contents = use(data);
  return contents.map((content) => <p>{content}</p>)
}

 

RSC를 가장 잘 지원하는 프레임 워크

 
RSC를 가장 잘 지원하는 프레임워크는 Next.js ^13.0.0 (App Router) 입니다. App Router에서의 컴포넌트는 기본적으로 서버 컴포넌트 입니다.
 
만약 App Router에서 클라이언트 컴포넌트를 사용하고자 한다면 파일 최상단에 ‘use client’ 지시문을 붙여줘야 합니다.
 

참고

https://ko.react.dev/reference/rsc/server-components

https://yceffort.kr/2022/01/how-react-server-components-work

https://react.dev/reference/react/use#use

https://velog.io/@2ast/React-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8React-Server-Component%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0