리액트 서버 컴포넌트

리액트 서버 컴포넌트(React Server Component, RSC)는 서버에서 리액트 컴포넌트를 렌더링하는 아키텍처 디자인이다.

리액트 서버 컴포넌트는 『Hypermedia Systems』의 저자들이 제안하는 방법과 매우 유사해 보인다. 물론 서버 컴포넌트와 htmx의 문제의식은 서로 다르고, 서버 컴포넌트가 하이퍼미디어의 고도화를 위해 만들어진 것도 아니지만, 서버에서 화면을 렌더링한다는 핵심 개념은 같지 않은가. 다양한 프레임워크의 아이디어가 서로 달라보이지만, 한 지점으로 수렴하고 있다. 결국 십수년을 돌고 돌아 다시 서버에서 화면을 렌더링하는 방식으로 돌아왔다는 생각도 든다.

배경

2015년 리액트가 처음 등장했을 때 대부분의 리액트 애플리케이션은 CSR(Cliend-side rendering)을 사용했다. 사용자는 아래와 같이 자바스크립트 번들 파일을 로드하는 간략한 HTML 파일을 제공받았다.

<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
  </body>
</html>

애플리케이션에 더 많은 기능이 추가될 수록 bundle.js의 크기는 더욱 커졌고, 사용자는 더 오래 로딩을 기다려야했다. 이러한 문제를 해결하기 위해 SSR(Server-side rendering)이 등장했다. SSR은 서버에서 틀이 갖춰진 HTML을 구성해 사용자에게 제공한다. 물론 여전히 자바스크립트 파일이 포함되어 있지만, 이 스크립트는 모든 DOM 노드를 만드는 대신 애플리케이션의 인터랙션만을 담당한다. 이 과정을 하이드레이션(Hydration)이라고 한다.

현대적인 CSR 웹 애플리케이션에서 클라이언트는 서버가 제공하는 API를 통해 데이터를 응답받고, 이 데이터를 이용해 콘텐츠를 렌더링한다. SSR 애플리케이션은 서버로부터 HTML을 받고, 하이드레이션을 거친다. 그 이후는 CRS 애플리케이션과 동일하다. API를 통해 데이터를 응답받고, 이를 이용해 콘텐츠를 렌더링한다. 사용자의 로딩 경험이 향상되지만, 궁극적인 차이를 만들지는 않는다.

만약 서버가 완성된 HTML을 제공해준다면 클라이언트가 서버에 추가적인 API 요청을 보내지 않아도 될 것이다. 이 문제에 대한 해결책이 NextJS, GatsbyJS, Remix와 같은 메타 프레임워크다. 하지만 이들에게도 트리의 최상위 컴포넌트만 서버에서 렌더링할 수 있다는 점, 메타 프레임워크의 접근방식이 서로 다르다는 점, 모든 리액트 컴포넌트가 항상 하이드레이트된다는 점 등의 한계가 있었다. 이에 대한 대안으로 리액트 서버 컴포넌트가 등장했다.

특징

서버 컴포넌트는 서버에서 단 한 번 실행되며, 자바스크립트 번들에 포함되지 않기 때문에 하이드레이션과 같은 과정도 거치지 않는다. 아래 코드는 서버 컴포넌트는 사용하는 예시로, UI 컴포넌트 안에 데이터베이스에서 데이터를 가져오는 코드가 포함되어 있는 것을 볼 수 있다.

async function App() {
  const data = await db.query('SELECT * FROM products');

  return (
    <>
      <h1>Products</h1>
      {data.map((item) => (
        <p key={item.id}>{item.title}</p>
      ))}
    </>
  );
}

그런데 서버 컴포넌트는 클라이언트에서 다시 렌더링되지 않기 때문에 컴포넌트의 상태를 다룰 수 없다. 상태를 다뤄야 한다면 클라이언트 컴포넌트를 사용해야 한다. 리액트 서버 컴포넌트 아키텍처에서는 모든 컴포넌트가 기본적으로 서버 컴포넌트로 취급되기 때문에 클라이언트 컴포넌트를 사용하려면 코드의 최상단에 'use client' 선언을 추가해야 한다. 이렇게 작성된 클라이언트 컴포넌트는 자바스크립트 번들에 포함되어 클라이언트에서 다시 렌더링된다.

상위의 클라이언트 컴포넌트에 변경된 상태를 하위의 서버 클라이언트로 전파할 수 있을까? 서버 컴포넌트는 클라이언트에서 다시 렌더링되지 않기 때문에 불가능하다. UI 트리에서 클라이언트 컴포넌트의 자식 노드는 클라이언트 컴포넌트이어야 한다는 규칙이 있다. 따라서 'use client'를 명시한 클라이언트 컴포넌트의 모든 하위 노드가 클라이언트 컴포넌트로 설정되는데, 이렇게 영향을 받는 범위를 클라이언트 바운더리(Client boundary)라고 한다. 이로 인해 애플리케이션의 최상단에서 상태를 관리한다면 모든 노드가 클라이언트 컴포넌트로 지정되는 문제가 있는데, 이 문제는 상태를 관리하는 컴포넌트를 별도로 분리하는 방식으로 해결할 수 있다.

관련문서

참고자료

이 문서를 인용한 문서

  • 리액트
  • Next.js
    • 앱 라우팅(App routing): 리액트 서버 컴포넌트, 스트리밍 등 추가 기능을 제공하는 새로운 라우팅 방식이다.