React Server Components

zero-bundle-size React Server Components 서버 드리븐 멘탈 모델을 통한 모던 UX를 구현하는데 초점이 맞추져 있으며 현재 React개발팀에서 개발이 진행 중이다. 이는 컴포넌트의 SSR과는 다르며 클라이언트 측 번들 사이즈를 꽤 작게 만들 수 있다.

이 업데이트는 아직 실무에서 사용이 어렵지만 매우 흥미로운 내용이라 계속 팔로잉해 보는것이 좋겠다. 아래는 관련된 내용들이다.

서버사이드 렌더링의 제약사항

오늘날 클라이언트측 자바스크립트의 서버측 렌더링은 차선책이다. 컴포넌트의 자바스크립트는 서버에서 HTML문자열로 렌더링 된다. HTML은 브라우저에게 전달되고 나타나며 빠른 FCP혹은 LCP를 가지게 된다.

하지만 hydration 단계를 통해 인터렉션이 가능해진 요소를 위해서는 여전히 자바스크립트가 필요하다. 서버사이드 렌더링은 일반적으로 최초 페이지 로드를 위해 사용되기 때문에 hydration이후에는 추가적으로 쓰이지 않는다.

클라이언트에서의 hydration을 전혀 하지 않고 React 와 SSR만 가지고 앱을 만들수도 있긴 하지만 앱의 모델에 인터렉션을 많이 해야 하는 경우에는 어쩔 수 없이 클라이언트 측으로 코드를 보내야 한다. 서버 컴포넌트의 하이브리드 모델은 이 선택을 컴포넌트 기준으로 가능하도록 해 준다.

React의 서버 컴포넌트를 활용하면 컴포넌트들을 정기적으로 다시 받아올 수 있다. 새로운 데이터가 있을 때 서버에서 앱의 컴포넌트들을 리-렌더할 수 있으므로 클라이언트 측에 전송해야 하는 코드량을 제한할 수 있다.

[RFC문서에서]: 개발자는 서드파티 패키지의 사용 여부를 지속적으로 검토해야 한다. 마크다운을 렌더링하거나, 날짜를 포멧하는 패키지를 사용하면 개발자는 편해질지 몰라도, 증가한 코드 사이즈로 인해 사용자는 성능에 손해를 볼 수 있다.

// *Before* Server Components
import marked from "marked"; // 35.9K (11.2K gzipped)
import sanitizeHtml from "sanitize-html"; // 206K (63.3K gzipped)

function NoteWithMarkdown({text}) {
  const html = sanitizeHtml(marked(text));
  return (/* render */);
}

서버 컴포넌트

React의 서버 컴포넌트는 중간에 추상화된 포멧에 렌더링 데이터를 포함하여 내려받는 방법으로 자바스크립트를 추가로 받지 않아도 되도록 서버 사이드 렌더링을 보완한다. 클라이언트에 렌더링 된 트리의 상태를 잃어버리지 않고 서버에서 렌더링된 트리를 병합할 수 있어 더 다양한 컴포넌트로 확장할 수 있다.

서버 컴포넌트가 SSR을 대체하지는 않는다. 두 기능이 함께 사용되면 중간 포멧을 활용한 빠른 렌더링을 지원하게 되고. 서버 사이드 렌더링 인프라가 이를 HTML로 렌더링하여 초기 페인트 속도는 여전히 빠르게 진행되도록 한다. SSR에서 외부 데이터를 조회하는 것 처럼. 서버 컴포넌트는 클라이언트 컴포넌트를 서버측에서 렌더링 한다.

하지만 이 때에는 자바스크립트 번들이 매우 작아진다. 페이지를 처음 탐색할의 번들 크기가 상당히 작아졌음 (-18~29%) 알 수 있었지만 React개발팀의 작업이 완료되면 이보다 더 번들 사이즈를 줄일 수 있게 될 것이다.

[RFC문서에서]: 위의 예제를 서버 컴포넌트로 마이그레이션할 경우 코드 자체는 이전과 동일하나 클라이언트에 코드를 전송하지 않게 된다. 예제에서는 압축하지 않았을 때 240K정도를 절약하고 있다.

import marked from 'marked' // zero bundle size
import sanitizeHtml from 'sanitize-html' // zero bundle size

function NoteWithMarkdown({ text }) {
  // same as before
}

자동 코드 스플리팅

코드 스플리팅을 통하여 사용자가 필요로하는 코드만 서빙하는것은 베스트 프렉티스로 고려된다. 이는 앱을 여러 작은 번들로 쪼개 클라이언트에 보내야 하는 코드를 최소화할 수 있게 된다. 서버 컴포넌트 전에는 React.lazy()를 코드 상에서 사용하여 코드를 나눠야 할 곳을 직접 표기했다.

// *Before* Server Components
import React from 'react'

// one of these will start loading *when rendered on the client*:
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'))
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'))

function Photo(props) {
  // Switch on feature flags, logged in/out, type of content, etc:
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {...props} />
  } else {
    return <PhotoRenderer {...props} />
  }
}

위와 같은 코드 스플리팅이 겪게 되는 이슈로는:

  • Next.js와 같은 외부 메타 프레임웍이 인지할 수 있도록 수동으로 지연 로딩 처리를 하려면 코드를 import문을 사용하는 동적 import로 변경해야 한다.
  • 앱이 컴포넌트를 로드하는것이 지연될 수 있어 UX에 영향을 끼친다.

서버 컴포넌트는 자동 코드 스플리팅을 도입하였는데. 이는 클라이언트 컴포넌트를 import하는 부분을 코드를 나눠야 할 수 있는 부분으로 인식한다. 또한 개발자들이 서버측 코드에서 어떤 컴포넌트를 더 일찍 받아야 하는지 지정할 수 있게 하여 클라이언트 측에서 조금 더 일찍 렌더링 프로세스를 시작할 수 있게 한다.

import React from 'react'

// one of these will start loading *once rendered and streamed to the client*:
import OldPhotoRenderer from './OldPhotoRenderer.client.js'
import NewPhotoRenderer from './NewPhotoRenderer.client.js'

function Photo(props) {
  // Switch on feature flags, logged in/out, type of content, etc:
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {...props} />
  } else {
    return <PhotoRenderer {...props} />
  }
}

서버 컴포넌트가 Next.js의 SSR을 대체할 수 있을까?

대체할 수 없다. 서버 컴포넌트와 SSR은 꽤 다르다. 서버 컴포넌트는 Next.js와 같은 메타-프레임웍에서 연구 및 실험이 진행중이며 실제로 실험적으로 도입될 것이다.

Next.js와 SSR의 차이점에 대해 댄이 언급한 내용을 정리해 보면:

  • 서버 컴포넌트의 코드는 클라이언트에 전송되지 않는다. 대부분 React SSR구현들은 컴포넌트 코드가 자바스크립트 번들에 포함되 클라이언트에 전송된다. 이는 인터렉션을 지연시킨다.
  • 서버 컴포넌트는 컴포넌트 트리 내 아무 곳이라도 백엔드에 접근할 수 있다. Next.js를 사용할때는 getServerProps()를 통하여 백엔드에 접근했지만 이 것은 페이지 단위로만 제한되어 있었다. npm에 올라간 모든 컴포넌트는 이렇게 할 수 없다.
  • 서버 컴포넌트는 클라이언트 컴포넌트 트리의 상태를 유지한채로 서버로부터 다시 받아올 수 있다. 이는 주요 전송 메커니즘이 HTML보다 더 다양한 케이스르 커버할 수 있기 때문이다. 따라서 검색 결과 텍스트, 포커스, 텍스트 선택 등의 클라이언트 상태를 날리지 않은 상태로 검색 결과 목록과 같은 서버렌더링 영역이 리패칭될 수 있다.

서버 컴포넌트의 초기 구현 중 일부는 다음과 같은 웹팩 플러그인을 통해 동작될 것이다.

  • 모든 클라이언트 측 컴포넌트를 찾아낸다
  • 각각의 ID와 분리된 코드의 ID를 매핑한다
  • Node.js의 로더는 클라이언트 컴포넌트의 import들을 위에서 만든 맵과 연관짓는다.
  • 작업 중 일부 (라우팅 같은 작업)은 React단독으로는 어렵기 때문에 Next.js와 같은 프레임웍과 함게 작동될 것이다.

댄이 위의 글에서 언급한 것 처럼 이 작업의 목표는 메타 프레임웍들이 훨씬 더 좋아지도록 하는 것이라 한다.

더 알아보거나 React 팀과 피드백 주고받기

이 내용에 대해서 더 알아보고 싶다면 댄과 로렌의 유툽 영상을 꼭 시청하기 바란다, RFC문서도 읽고 서버 컴포넌트의 데모도 돌려 보고 말이다. 고맙게도 Sebastian Markbåge, Lauren Tan, Joseph Savona, Dan Abramov가 서버 컴포넌트를 작업하고 있다.

흥미로운 관련 쓰레드 읽기: