리액트 서버 컴포넌트란 도대체 무엇일까요?

알쏭달쏭한 리액트 서버 컴포넌트를 정리합니다.

2023-09-15

php 아냐?

개요

리액트 서버 컴포넌트가 무엇인지 진득하니 알아봅시다.

우선 리액트 서버 컴포넌트가 무엇인지 알기 전에, 클라이언트 UI 라이브러리로써의 리액트와 서버 사이드 렌더링을 이해하는 것이 좋습니다.

클라이언트 UI 라이브러리로써 리액트

우리에게 가장 익숙한 형태의 리액트는 클라이언트 UI 라이브러리로써의 리액트일 것입니다. 말 그대로 클라이언트에 필요한 UI를 리액트를 사용해 그릴 수 있습니다. 이때, 리액트는 컴포넌트 기반의 개발을 가능하게 해줍니다. 이를 통해 개발자들은 하나의 거대한 모놀리식 컴포넌트를 작은 컴포넌트 단위로 쪼갤 수 있습니다.

components

이렇게 쪼개진 컴포넌트들은 그들의 스냅샷 데이터인 state와 다른 컴포넌트로 전달되는 prop을 갖습니다. 이 컴포넌트들을 통해 우리는 유저와 상호작용할 수 있게 됩니다. 대부분의 리액트 앱은 클라이언트 사이드 앱이며, 모든 컴포넌트(클래스 컴포넌트를 제외한)의 경우 자바스크립트 함수입니다.

즉, 브라우저에 앱이 로드 되고, 컴포넌트가 포함된 자바스크립트 번들의 다운로드와 파싱이 완료되어야만 앱이 정상적으로 동작하게 됩니다. 이와 같이 클라이언트 측에서 자바스크립트가 다운로드 되고 파싱 되는 방식을 클라이언트 사이드 렌더링 (CSR) 이라고 합니다.

이때 사용되는 우리에게 익숙한 컴포넌트를 클라이언트 컴포넌트라고 부를 수 있습니다.

클라이언트 사이드 렌더링은 빠르게 유저와 상호작용할 수 있다는 장점이 있지만, 위와 같은 일련의 과정이 모두 끝나야 유저가 실제로 원하는 컨텐츠를 볼 수 있다는 단점이 있습니다. 즉 유저는 기다리는 동안 텅 빈 화면을 보게 됩니다. 우리가 코드를 추가하거나 새로운 라이브러리를 설치할 경우 자바스크립트 번들이 커지게 되는데, 이는 유저가 더 기다려야 하는 치명적인 경험을 제공하게 됩니다.

그렇다면 이런 문제를 어떻게 개선해 나갈 수 있을까요? 바로 서버 사이드 렌더링을 통해 이런 문제들을 개선할 수 있습니다.

서버 사이드 렌더링 (SSR)

SSR은 빈 HTML 문서를 보내는 대신, 서버에서 실제 HTML을 생성해 앱을 렌더 하는 것입니다. SSR은 초기 페이지 로드에 중점을 둡니다. 서버는 프리렌더된 HTML을 클라이언트로 전달합니다. 하지만, HTML은 클라이언트에서 유저와 상호작용하기 위해선 다운로드된 자바스크립트의 하이드레이션이 필요합니다.

하이드레이션이란 건조한 HTML에 물을 주는 것으로, 여기서 물이란 상호작용과 이벤트 핸들러를 의미합니다.

즉, 자바스크립트 번들이 다운되고 파싱 되는 동안 유저가 빈 페이지를 보지 못하도록 서버에서 HTML을 생성하고, 하이드레이션을 통해 물을 뿌리는 것이 SSR라고 할 수 있습니다. CSR과 달리, 초기에 유저가 빠르게 컨텐츠를 볼 수 있다는 개선점이 있으나 역시나 SSR의 문제점이 존재합니다. 바로 "All or nothing" 워터폴 문제입니다.

문제들은 다음과 같습니다:

모든 데이터는 보여지기 전 서버에서 반드시 페치되어야한다.

예를 들어, 리뷰가 있는 상품 페이지를 렌더 하고 싶습니다. 이 리뷰가 우선적으로 보이는 것이 중요해, 서버의 결과물로 포함시킵니다. 하지만, 데이터베이스 또는 API가 느린 불가항력의 상황에 놓였습니다. 이런 상황에서, 우리는 선택을 내려야 합니다. 만약 서버에서 제외한다면 우리는 자바스크립트가 전부 로드된 후에야 리뷰를 볼 수 있습니다. 그러나, 서버에 포함하는 경우 리뷰가 로드되어야만 나머지 HTML을 렌더 할 수 있습니다.

모든 자바스크립트는 클라이언트에서 하이드레이트 되기 전에 반드시 다운로드 되어야한다.

예를 들어, 리뷰 컴포넌트에 상호작용에 관한 복잡한 로직들이 존재하고 자바스크립트가 로드되는 데 시간이 걸린다고 가정합니다. 리뷰를 SSR을 사용해 유저에게 먼저 보여주는 것은 좋습니다. 그러나, 하이드레이션은 오직 한 번의 패스로 완료되기에, 네비게이션 바, 사이드바, 상품 컨텐츠의 하이드레이션이 완료되기 전까지 리뷰의 하이드레이션을 시작할 수 없습니다.

모든 하이드레이션이 완료되기 전까지, 클라이언트에서 어떤 것도 상호작용할 수 없다.

리액트는 한 번의 하이드레이션이 시작되면, 완료할 때까지 멈추지 않고 무조건 진행됩니다. 예를 들어, 리뷰 컴포넌트에 매우 복잡한 렌더링 로직이 존재해 화면에 보여지는 데 몇 초의 시간이 걸린다고 가정합니다. 이 경우, 하이드레이션이 완료되지 않았기에 유저는 네비게이션 바, 사이드바, 상품 컨텐츠 등에서 상호작용을 할 수 없게 됩니다. 특히 네비게이션의 경우 치명적인데, 유저는 원치 않는 페이지에 머물게 됩니다.

위와 같은 문제를 리액트는 서버 사이드 스트리밍과 선택적인 하이드레이션이 가능한, <Suspense>라는 기능을 통해 해결하기로 하였습니다. 컴포넌트를 <Suspense>로 감싸, 리액트에게 컴포넌트의 렌더링과 하이드레이션의 우선순위가 낮다고 알려줄 수 있습니다. 또, 다른 컴포넌트의 로드가 느린 컴포넌트에 의해 막히지 않게 됩니다.

그렇다면 <Suspense>를 사용하면, 모든 문제가 해결되는 것이 아닐까요? 개선점은 충분히 있지만 몇 가지 이슈가 존재합니다.

  1. 데이터 페치를 위해 서버 - 클라이언트 간 너무 긴 왕복 여행을 필요로 한다.
  2. 브라우저에서 비동기적으로 스트리밍 되더라도, 모든 자바스크립트는 결국 다운로드된다.
  3. 하이드레이션이 최적화되더라도, 유저는 클라이언트 사이드 자바스크립트가 다운로드 될 때까지 상호작용하지 못한다.
  4. 높은 비중의 자바스크립트가 여전히 클라이언트에 존재한다.

이와 같은 문제를 해결하기 위해 등장한 것이 바로 리액트 서버 컴포넌트입니다.

리액트 서버 컴포넌트

리액트의 새로운 서버 컴포넌트는 서버 사이드 렌더링을 보완하여 자바스크립트 번들에 추가하지 않고도 렌더링 할 수 있게 합니다. 또한, 서버에서만 실행되므로 서버 컴포넌트를 사용하면 UI를 서버에서 렌더 할 수 있고 컴포넌트 내부에서 직접 데이터를 페치할 수 있습니다.

즉 리액트 서버 컴포넌트는 오직 서버에서 실행되는 컴포넌트라고 할 수 있습니다.

서버 컴포넌트의 장점

  • 데이터 페칭: 서버 컴포넌트는 데이티 페칭을 데이터 소스와 가까운, 서버로 옮길 수 있게 해줍니다. 이것을 통해 렌더링에 필요한 페치의 소요 시간, 클라이언트에서 만드는 요청의 양을 줄일 수 있습니다.
  • 캐싱: 서버에서 렌더링 되기에, 후속 요청과 유저 전반에 걸쳐 재사용할 수 있습니다.
  • 번들 사이즈: 서버 컴포넌트는 클라이언트 자바스크립트 번들에 영향을 미치는 큰 의존성들을 리졸브 할 수 있습니다. 즉 처리된 결과만을 클라이언트에 전달하므로 클라이언트에서 다운로드하지 않아도 되므로, 인터넷이 느리거나 성능이 좋지 못한 장치를 갖는 유저들에게 유용합니다.
  • 초기 페이지 로드와 First Contentful Paint: 서버에서 HTML을 생성하기에, 클라이언트에서 자바스크립트의 다운로드와 파싱 필요 없이 즉시 페이지를 볼 수 있습니다.
  • 스트리밍: 서버 컴포넌트는 렌더링 작업을 청크 단위로 쪼개고 준비가 되면 클라이언트로 스트리밍 합니다. 유저는 전체 페이지가 서버에서 렌더 되는 것을 기다릴 필요 없이 페이지의 부분을 볼 수 있게 됩니다.
  • SEO: 렌더 된 HTML은 서치 엔진 봇들에게 수집될 수 있고 소셜 네트워크 봇들이 페이지의 프리뷰를 생성할 수 있게 해줍니다.

서버 컴포넌트의 한계

리액트 서버 컴포넌트는 서버에서만 실행됩니다. 이들은 클라이언트 사이드와 관련된 어떠한 기능을 갖고 있지 않습니다. 서버 컴포넌트를 사용하면 유저와의 상호작용을 추가할 수 없습니다. 예를 들어, 이벤트 핸들러 부착 또는 리액트 훅을 사용할 수 없습니다.

만약 컴포넌트가 클라이언트 상호작용에 의존한다면, 클라이언트 컴포넌트를 사용해야 합니다.
즉, 우리가 기존에 사용하던 리액트의 컴포넌트(훅, 유저와 상호작용)는 전부 클라이언트 컴포넌트입니다.

넥스트에서 리액트 서버 컴포넌트

현재 리액트 서버 컴포넌트를 사용하려면 메타-프레임워크인 넥스트를 사용해야 합니다.

넥스트 13.4 버전이 배포되며, 앱 라우터가 스테이블한 기능이 되었습니다. 넥스트를 사용하면 컴포넌트는 기본적으로 서버 컴포넌트입니다.

만약 넥스트에서 클라이언트 컴포넌트를 사용하고 싶다면 "use client" 지시어를 최상단(import 위)에 추가하면 됩니다.

데이터 페치 관점에서 CSR과 SSR의 차이점 살펴보기

데이터 페칭을 하기 위해선 데이터를 보여주는 클라이언트 사이드의 리액트 앱과 서버 사이드 REST API 앱이 필요합니다. 리액트에서 데이터를 페치하기 위해선 useEffect 훅, React-Query, SWR 등을 사용해 백엔드로 네트워크 요청을 해야 합니다. 이때 데이터베이스에서 데이터를 집어 네트워크를 통해 전달합니다.

csr-fetch

위 그래프는 클라이언트 사이드 렌더링의 패턴을 나타냅니다. 클라이언트가 HTML 파일을 받는 시점에서 시작됩니다. 이 파일은 어떤 컨텐츠도 포함하지 않으며, 하나 혹은 그 이상의 스크립트 태그를 갖습니다.

자바스크립트 다운로드되고 파싱 되면, 리액트 앱이 실행되고, 돔 노드를 생성해 UI를 채웁니다. 초기에는, 유저는 어떠한 실제 데이터도 갖지 못하고, 로딩 상태와 오직 앱의 쉘(레이아웃)만 렌더 합니다. 유저는 네트워크 요청이 완료될 때까지 로딩 상태를 보게 되고, 리렌더를 통해 로딩 UI(스피너, 스켈레톤 등)가 실제 컨텐츠로 대체됩니다.

다음은 SSR을 사용한 페치 패턴입니다.

ssr-fetch

클라이언트 사이드와 다른 점은 바로 초기 렌더를 서버에서 한다는 것입니다. 즉 유저가 텅 빈 HTML을 받지 않는다는 것입니다. 텅 빈 페이지를 보는 것보단 확실한 성능의 향상이 존재하지만, 실제로는 눈에 뛸 정도로 향상된 것은 아닙니다. 유저는 로딩 UI가 아닌 컨텐츠를 보기 위해 우리의 앱을 방문합니다.

초기에 서버에서 렌더 하게 되면, 좀 더 빠르게 쉘을 볼 수 있게 됩니다. 우리는 텅 빈 페이지보다 상황이 진행된다고 느끼기에 로딩 경험이 좀 더 빠르게 느껴질 수 있습니다.

하지만 위와 같은 흐름은 뭔가 이상합니다. 클라이언트와 서버를 왕복하지 않고, 데이터베이스 쿼리를 초기에 서버에서 요청한다면 컨텐츠가 온전히 채워진 UI를 즉시 유저에게 보낼 수 있을 것입니다. 이때, 리액트 서버 컴포넌트를 사용하면 이런 문제를 해결할 수 있습니다.

리액트 서버 컴포넌트를 사용해 데이터 페치하기

리액트 서버 컴포넌트를 사용하면 컴포넌트 내부에서 데이터 페치와 데이터베이스 쿼리를 할 수 있게 해줍니다. 데이터 페치 로직 자체를 서버로 옮길 수 있기에, 클라이언트에서는 데이터와 함께 구성된 컴포넌트를 받습니다.

서버 컴포넌트를 사용하면 다음과 같이 코드를 작성할 수 있습니다.

import db from "some-db";
 
async function Home() {
  const data = await db.findMany();
 
  return (
    <section>
      {data.map((item) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </section>
  );
}
rsc-fetch

리액트 서버 컴포넌트를 사용하면 위와 같이 클라이언트와 서버 왕복 없이도 데이터의 흐름을 만들어 낼 수 있습니다. 이를 통해 유저는 로딩 UI 없이 바로 컨텐츠를 페이지에서 볼 수 있게 됩니다.

서버 사이드 렌더링과 서버 컴포넌트의 차이점

서버 컴포넌트는 서버 사이드 렌더링을 대체하는 것이 아닙니다. 여전히 초기 HTML을 만들기 위해선 서버 사이드 렌더링에 의존해야 합니다. 리액트 서버 컴포넌트는 컴포넌트가 클라이언트 사이드 번들에서 제외될 수 있게 해주며, 오직 서버에서만 실행되는 것을 보장합니다.

  • 서버 컴포넌트의 코드는 절대로 클라이언트로 전달되지 않습니다. 리액트를 사용해 SSR을 구현 시, 컴포넌트의 코드는 어쨋든 클라이언트 번들로 전달됩니다. 이것은 상호작용을 지연시킵니다.
  • 서버 컴포넌트는 트리 어디에서든 백엔드에 접근할 수 있습니다. 기존 page 라우터의 getServersideProps을 사용하는 방식의 경우 최상위 페이지에서만 가능하였습니다.
  • 서버 컴포넌트는 트리 내부에서 클라이언트 사이드 상태를 유지한 채로 리페치할 수 있습니다. 이는 HTML의 상태(검색 입력 텍스트, 포커스 등)를 초기화하지 않고, 서버에서 렌더 된 부분을 가져올 수 있어 HTML을 더욱 풍부하게 합니다.

이 둘을 같이 사용하는 것은 서로에게 시너지 효과를 내는 것이라고 생각할 수 있습니다. (초밥과 와사비 처럼 🍣)

Recap

리액트 서버 컴포넌트를 사용하면 서버에서만 코드를 실행할 수 있습니다. 자바스크립트 번들에 포함되지 않아, 다운로드 되어야 하는 자바스크립트와 하이드레이트 되어야 하는 컴포넌트의 양을 줄일 수 있습니다.

상호작용이 필요 없는 컴포넌트이고 서버 컴포넌트가 될 수 있다면, 리액트 서버 컴포넌트를 사용하는 것이 성능에 더 좋습니다. 또, 서버에 직접적으로 접근할 수 있기에 데이터 페치하는 시간을 줄여줍니다.

여전히 클라이언트 컴포넌트를 사용해, 기존 리액트의 방식(상호작용, SSR 하이드레이션)을 사용할 수 있습니다. 용도에 맞는 컴포넌트를 선택해 우리는 하이브리드 애플리케이션을 만들 수 있게 되었습니다.


출처 및 참조

  • https://react.dev/blog/2020/12/21/data-fetching-with-react-server-components
  • https://github.com/reactwg/react-18/discussions/37
  • https://vercel.com/blog/understanding-react-server-components
  • https://nextjs.org/docs/app/building-your-application/rendering/server-components
  • https://www.joshwcomeau.com/react/server-components
  • https://www.patterns.dev/posts/react-server-components