React 서버 컴포넌트


원문: https://addyosmani.com/blog/react-server-components/

이번 주, React 팀은 서버 주도(Server-Driven) 멘탈 모델로 모던 UX를 가능하게 하는 것을 목표로 하는 zero-bundle-size React 서버 컴포넌트를 시연했다. 서버 컴포넌트는 서버 사이드 렌더링(SSR)과는 상당히 다르며 클라이언트 사이드 번들 크기를 매우 줄일 수 있다.

필자는 이 작업 방향에 대해 매우 기뻤고, 아직 정식 출시하지는 않았지만 계속 지켜볼 가치가 있다고 생각했다. 자세한 내용은 RFC를 읽거나 Dan과 Lauren의 강연을 듣는 것을 추천한다.

서버 사이드 렌더링의 한계

오늘날 서버 사이드 렌더링은 클라이언트 사이드 렌더링의 차선책일 수 있다. 컴포넌트를 구현하기 위한 자바스크립트 코드는 서버에서 HTML 문자열로 렌더링 된다. 이 HTML은 브라우저로 전달되어 빠른 속도로 First Contentful Paint 또는 Largest Contentful Paint로 나타날 수 있다.

그러나, 여전히 상호작용을 위한 hydration 단계가 종종 필요하기 때문에 자바스크립트 코드를 가져와야 한다. 일반적으로 서버 사이드 렌더링은 초기 페이지를 로드에 사용되므로, hydration 후에는 다시 사용할 수 없다.

📘 Note: SSR을 활용하여 클라이언트에서 hydrate를 사용하지 않는 서버 전용 React 앱을 구축할 수 있는 것은 사실이지만, 모델에서의 과도한 상호작용은 React를 벗어난 것을 수반하는 경우가 많다. 서버 컴포넌트를 사용한 하이브리드 모델은 각 컴포넌트별로 이를 결정하게 할 수 있다.

React 서버 컴포넌트를 사용하면, 컴포넌트를 정기적으로 다시 가져올 수 있다. 새 데이터가 있을 때 리렌더링 되는 컴포넌트가 서버에서 실행되므로 클라이언트에 전송되는 코드의 양을 제한 할 수 있다.

[RFC]: 개발자들은 항상 서드파티 패키지를 사용할지 말지 결정해야 한다. 서드파티 패키지를 사용해 마크다운을 일부 렌더링하거나 날짜를 포맷팅 하는 것은 개발자들에게 편리할 수 있지만, 코드의 크기가 커지고 사용자의 체감 성능을 저하시킨다.

// NoteWithMarkdown.js: 서버 컴포넌트 *사용 전*

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

서버 컴포넌트

서버 컴포넌트는 서버 사이드 렌더링을 보완하여 자바스크립트 번들에 추가할 필요 없이 중간 추상 형식으로 렌더링 할 수 있게 한다. 이를 통해 상태(state) 손실 없이 서버 트리를 클라이언트 트리와 병합할 수 있으며 더 많은 컴포넌트로 확장할 수 있다.

서버 컴포넌트는 SSR을 대체하지 않는다. SSR과 서버 컴포넌트가 함께 사용된다면, 중간 형식으로 빠르게 렌더링한 다음, 서버 사이드 렌더링을 통해 이를 HTML로 렌더링해 초기 페인트를 여전히 빠르게 구현할 수 있다. SSR이 다른 데이터를 받아오는(fetch) 메커니즘과 함께 사용되는 방식과 비슷하게, 서버 컴포넌트가 만든 클라이언트 컴포넌트를 SSR 한다.

하지만, 이 경우에는 자바스크립트 번들 크기가 훨씬 작아질 것이다. 초기 조사 결과, 번들 크기에서 상당한 성과(-18-29%)를 거둘 수 있는 것으로 나타났지만, React 팀은 기반 작업만 완료되면 React팀은 야생(wild)에서 살아남는 확실한 아이디어를 얻게 될 것이다.

[RFC]: 위 예제를 서버 컴포넌트로 마이그레이션 하면 기능에 대해 똑같은 코드로 사용할 수 있지만 클라이언트 측으로 보내지 않아도 된다. 이를 통해 코드를 240K(압축되지 않았을 때) 이상 절약할 수 있다.

// NoteWithMarkdown.server.js - Server Component === zero bundle size

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

function NoteWithMarkdown({text}) {
  // 이전과 같다.
}

자동 코드 분할(Code-splitting)

코드 분할을 통해 사용자가 필요로 하는 코드만을 제공하는 것은 모범 사례로 여겨진다. 코드 분할을 통해 클라이언트에 보낼 필요가 있는 작은 번들로 나눌 수 있었다. 서버 컴포넌트 이전에는 일일히 React.lazy()를 이용해 "분할 지점"을 정의하거나 routes/pages 같은 메타 프레임워크에 의해 설정된 휴리스틱을 사용해 새 청크를 생성했다.

// PhotoRenderer.js (서버 컴포넌트 사용 전)
import React from 'react';

// 이 중 하나는 *클라이언트 렌더링 될 때* 로딩을 시작한다.
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js'));
const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));

function Photo(props) {
  // 로그인, 로그아웃, 컨텐츠 유형 등 기능 플래그 켜기
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {...props} />; 
  } else {
    return <PhotoRenderer {...props} />;
  }
}

코드 분할에는 다음과 같은 몇 가지 문제가 있다.

  • (Next.js 같은)메타 프레임워크 외부에서는, import 구문을 dynamic import 구문으로 대체해 최적화 작업을 수동으로 처리해야 하는 경우가 많다.
  • 유저 경험에 영향을 주는 컴포넌트를 로드하기 시작하면 지연될 수 있다.

서버 컴포넌트는 클라이언트 컴포넌트의 모든 일반 import를 코드 분할 가능 지점으로 처리하는 자동 코드 분할을 도입한다. 또한, 개발자는 (서버에서) 훨씬 이전에 사용할 컴포넌트를 선택할 수 있게 되어, 렌더링 프로세스 초기에 클라이언트가 더 일찍 가져올 수 있게 된다.

// PhotoRenderer.server.js - 서버 컴포넌트
import React from 'react';


// 이 중 하나가 *렌더링 되고 클라이언트로 스트리밍 되면* 로드를 시작한다.
import OldPhotoRenderer from './OldPhotoRenderer.client.js';
import NewPhotoRenderer from './NewPhotoRenderer.client.js';

function Photo(props) {
  // 로그인, 로그아웃, 컨텐츠 유형 등 기능 플래그 켜기
  if (FeatureFlags.useNewPhotoRenderer) {
    return <NewPhotoRenderer {...props} />;
  } else {
    return <PhotoRenderer {...props} />;
  }
}

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

아니다. 둘은 많은 부분이 다르다. 서버 컴포넌트의 초기 채택은 연구 및 실험이 계속됨에 따라 Next.js 같은 실제 메타 프레임워크에서 이뤄질 것이다.

Dan Abramov의 서버 컴포넌트와 Next.js의 다른 점에 대한 좋은 설명을 요약하면 다음과 같다.

  • 서버 컴포넌트 코드는 절대 클라이언트에게 전달되지 않는다. 많은 React를 사용한 SSR의 구현은 자바스크립트 번들을 통해 클라이언트로 컴포넌트 코드가 보내지게 된다. 이로 인해 상호작용이 지연될 수 있다.
  • 서버 컴포넌트를 사용하면 트리의 어느 곳에서나 백엔드에 접근할 수 있다. Next.js를 사용한다면, 최상위 페이지에서만 가능한 getServerProps()를 통해 백엔드에 접근하는 것에 익숙할 것이다. 하지만, 임의 npm 컴포넌트는 이런 동작이 불가능하다.
  • 트리 내부에서 클라이언트 측의 상태(state)를 유지하면서 서버 컴포넌트를 다시 가져올 수 있다. 이는 주요 전송 메커니즘이 HTML보다 훨씬 풍부하기 때문이다. 따라서, 내부 상태(e.g 검색 입력 텍스트, 포커스, 텍스트 선택)를 없애지 않고 서버에서 렌더링 한 부분(e.g 검색 결과 목록)을 다시 가져올 수 있게 한다.

서버 컴포넌트는 웹팩 플러그인을 통해 초기 통합 작업 중 일부를 수행한다.

  • 모든 클라이언트 컴포넌트 찾기
  • IDs => 청크 URLs 간의 매핑 만들기
  • Node.js 로더를 통해 클라이언트 컴포넌트로 import하는 부분을 이 맵에 대한 참조로 대체한다.
  • 이 작업 중 일부는 더 깊은 통합(e.g 라우팅과 같은 부분)이 필요하므로 Next.js와 같은 프레임워크와 함께 작동하도록 하는 것이 중요하다.

Dan이 언급했듯이, 작업의 목표 중 하나는 메타 프레임워크를 훨씬 더 향상시키는 것이다.

더 배우고 피드백을 React 팀과 공유하고 싶다면

이 작업에 대해 더 알아보려면, Dan과 Lauran의 강연을 보고 RFC를 읽은 뒤 서버 컴포넌트 데모를 확인하도록 하자. 마지막으로 서버 컴포넌트 작업을 진행해 준 Sebastian Markbåge, Lauren Tan, Joseph Savona 그리고 Dan Abramov에게 감사를 표한다.

관련된 흥미로운 글이 더 궁금하다면

한정2021.01.19
Back to list