한국어

React 18의 선택적 하이드레이션으로 웹 성능을 극대화하세요. 우선순위 기반 로딩, 스트리밍 SSR, 그리고 실용적인 구현 방법을 종합적으로 안내합니다.

React 선택적 하이드레이션: 우선순위 기반 컴포넌트 로딩 심층 분석

우수한 웹 성능을 향한 끊임없는 노력 속에서, 프론트엔드 개발자들은 항상 복잡한 트레이드오프 관계를 마주합니다. 우리는 풍부하고 상호작용이 가능한 애플리케이션을 원하면서도, 사용자의 기기나 네트워크 속도에 관계없이 즉시 로드되고 지연 없이 반응하기를 바랍니다. 수년간 서버 사이드 렌더링(SSR)은 빠른 초기 페이지 로드와 강력한 SEO 이점을 제공하며 이러한 노력의 핵심이었습니다. 하지만 전통적인 SSR에는 '전부 아니면 전무(all-or-nothing)'라는 치명적인 하이드레이션 병목 현상이라는 문제가 있었습니다.

SSR로 생성된 페이지가 진정으로 상호작용 가능해지기 전에는, 전체 애플리케이션의 자바스크립트 번들을 다운로드하고, 파싱하고, 실행해야만 했습니다. 이로 인해 페이지가 완전히 준비된 것처럼 보이지만 클릭이나 입력에 반응하지 않는 답답한 사용자 경험이 종종 발생했으며, 이는 TTI(Time to Interactive)나 더 새로운 지표인 INP(Interaction to Next Paint)와 같은 핵심 지표에 부정적인 영향을 미쳤습니다.

React 18이 등장했습니다. 획기적인 동시성 렌더링 엔진을 통해 React는 강력하면서도 우아한 해결책인 선택적 하이드레이션(Selective Hydration)을 도입했습니다. 이것은 단순한 점진적 개선이 아니라, React 애플리케이션이 브라우저에서 생명력을 얻는 방식에 대한 근본적인 패러다임 전환입니다. 이는 모놀리식(monolithic) 하이드레이션 모델에서 벗어나 사용자 상호작용을 최우선으로 하는 세분화된 우선순위 기반 시스템으로 나아갑니다.

이 종합 가이드에서는 React 선택적 하이드레이션의 메커니즘, 이점, 그리고 실제 구현 방법을 탐구할 것입니다. 우리는 그것이 어떻게 작동하는지, 왜 글로벌 애플리케이션에 있어 게임 체인저인지, 그리고 이를 활용하여 더 빠르고 회복력 있는 사용자 경험을 구축하는 방법을 분석할 것입니다.

과거의 이해: 전통적인 SSR 하이드레이션의 과제

선택적 하이드레이션의 혁신을 제대로 이해하려면, 먼저 그것이 극복하고자 했던 한계를 알아야 합니다. React 18 이전의 서버 사이드 렌더링 세계로 돌아가 보겠습니다.

서버 사이드 렌더링(SSR)이란 무엇인가?

일반적인 클라이언트 사이드 렌더링(CSR) React 애플리케이션에서는 브라우저가 최소한의 HTML 파일과 큰 자바스크립트 번들을 받습니다. 그런 다음 브라우저는 자바스크립트를 실행하여 페이지 콘텐츠를 렌더링합니다. 이 과정은 느릴 수 있어 사용자가 빈 화면을 쳐다보게 만들고, 검색 엔진 크롤러가 콘텐츠를 인덱싱하기 어렵게 만듭니다.

SSR은 이 모델을 뒤집습니다. 서버가 React 애플리케이션을 실행하여 요청된 페이지의 전체 HTML을 생성하고 브라우저로 보냅니다. 그 이점은 즉각적입니다:

'전부 아니면 전무' 하이드레이션 병목 현상

SSR로부터 받은 초기 HTML은 빠른 비상호작용적 미리보기를 제공하지만, 페이지는 아직 진정으로 사용 가능하지 않습니다. React 컴포넌트에 정의된 이벤트 핸들러(`onClick` 등)와 상태 관리가 빠져있기 때문입니다. 이 자바스크립트 로직을 서버에서 생성된 HTML에 붙이는 과정을 하이드레이션(hydration)이라고 합니다.

여기에 고전적인 문제가 있습니다. 전통적인 하이드레이션은 모놀리식하고, 동기적이며, 블로킹(blocking) 작업이었습니다. 다음과 같이 엄격하고 예외 없는 순서를 따랐습니다:

  1. 페이지 전체에 대한 자바스크립트 번들 전체를 다운로드해야 합니다.
  2. React가 번들 전체를 파싱하고 실행해야 합니다.
  3. 그런 다음 React는 루트부터 전체 컴포넌트 트리를 순회하며 모든 단일 컴포넌트에 이벤트 리스너를 붙이고 상태를 설정합니다.
  4. 이 모든 과정이 완료된 후에야 페이지가 상호작용 가능해집니다.

완전히 조립된 멋진 새 차를 받았지만, 차량 전체 전자 장치의 마스터 스위치 하나가 켜지기 전까지는 문 하나도 열 수 없고, 시동을 걸 수도, 경적을 울릴 수도 없다고 상상해 보세요. 단지 조수석에서 가방을 꺼내고 싶을 뿐인데도 모든 것을 기다려야 합니다. 이것이 바로 전통적인 하이드레이션의 사용자 경험이었습니다. 페이지는 준비된 것처럼 보이지만, 어떤 상호작용 시도도 아무런 결과를 낳지 않아 사용자의 혼란과 "분노의 클릭(rage clicks)"을 유발했습니다.

React 18의 등장: 동시성 렌더링으로의 패러다임 전환

React 18의 핵심 혁신은 동시성(concurrency)입니다. 이를 통해 React는 여러 상태 업데이트를 동시에 준비하고, 메인 스레드를 막지 않으면서 렌더링 작업을 일시 중지, 재개 또는 중단할 수 있습니다. 이것이 클라이언트 사이드 렌더링에 미치는 영향도 크지만, 훨씬 더 스마트한 서버 렌더링 아키텍처를 여는 열쇠이기도 합니다.

동시성은 선택적 하이드레이션을 가능하게 하는 두 가지 핵심 기능을 함께 작동시킵니다:

  1. 스트리밍 SSR: 서버는 전체 페이지가 준비될 때까지 기다리는 대신, 렌더링되는 대로 HTML을 청크(chunk) 단위로 보낼 수 있습니다.
  2. 선택적 하이드레이션: React는 전체 HTML 스트림과 모든 자바스크립트가 도착하기 전에도 페이지 하이드레이션을 시작할 수 있으며, 이를 논블로킹(non-blocking) 및 우선순위 방식으로 수행할 수 있습니다.

핵심 개념: 선택적 하이드레이션이란 무엇인가?

선택적 하이드레이션은 '전부 아니면 전무' 모델을 해체합니다. 하나의 거대한 작업 대신, 하이드레이션은 더 작고, 관리 가능하며, 우선순위를 정할 수 있는 일련의 작업이 됩니다. 이를 통해 React는 컴포넌트가 사용 가능해지는 대로 하이드레이션하고, 가장 중요하게는 사용자가 적극적으로 상호작용하려는 컴포넌트의 우선순위를 높일 수 있습니다.

핵심 요소: 스트리밍 SSR과 ``

선택적 하이드레이션을 이해하려면, 먼저 두 가지 기본 기둥인 스트리밍 SSR과 `` 컴포넌트를 파악해야 합니다.

스트리밍 SSR

스트리밍 SSR을 사용하면, 서버는 초기 HTML을 보내기 전에 느린 데이터 로딩(예: 댓글 섹션을 위한 API 호출)이 완료될 때까지 기다릴 필요가 없습니다. 대신, 메인 레이아웃이나 콘텐츠처럼 준비된 페이지 부분의 HTML을 즉시 보낼 수 있습니다. 느린 부분에 대해서는 플레이스홀더(폴백 UI)를 보냅니다. 느린 부분의 데이터가 준비되면, 서버는 추가 HTML과 인라인 스크립트를 스트리밍하여 플레이스홀더를 실제 콘텐츠로 교체합니다. 이는 사용자가 페이지 구조와 주요 콘텐츠를 훨씬 더 빨리 볼 수 있음을 의미합니다.

`` 경계

`` 컴포넌트는 애플리케이션의 어느 부분이 나머지 페이지를 막지 않고 비동기적으로 로드될 수 있는지 React에게 알려주는 메커니즘입니다. 느린 컴포넌트를 ``로 감싸고 `fallback` prop을 제공하면, React는 컴포넌트가 로딩되는 동안 이 폴백을 렌더링합니다.

서버에서 ``는 스트리밍을 위한 신호입니다. 서버가 `` 경계를 만나면, 먼저 폴백 HTML을 보내고 실제 컴포넌트의 HTML은 준비되었을 때 나중에 스트리밍할 수 있다는 것을 알게 됩니다. 브라우저에서 `` 경계는 독립적으로 하이드레이션될 수 있는 '섬(island)'을 정의합니다.

개념적인 예시는 다음과 같습니다:


function App() {
  return (
    <div>
      <Header />
      <main>
        <ArticleContent />
        <Suspense fallback={<CommentsSkeleton />}>
          <CommentsSection />  <!-- 이 컴포넌트는 데이터를 가져올 수 있습니다 -->
        </Suspense>
      </main>
      <Suspense fallback={<ChatWidgetLoader />}>
        <ChatWidget /> <!-- 이것은 무거운 서드파티 스크립트입니다 -->
      </Suspense>
      <Footer />
    </div>
  );
}

이 예시에서 `Header`, `ArticleContent`, `Footer`는 즉시 렌더링되고 스트리밍됩니다. 브라우저는 `CommentsSkeleton`과 `ChatWidgetLoader`에 대한 HTML을 받게 됩니다. 나중에 서버에서 `CommentsSection`과 `ChatWidget`이 준비되면, 해당 HTML이 클라이언트로 스트리밍됩니다. 이러한 `` 경계는 선택적 하이드레이션이 마법을 부릴 수 있도록 하는 '이음새'를 만듭니다.

작동 방식: 우선순위 기반 로딩의 실제

선택적 하이드레이션의 진정한 탁월함은 사용자 상호작용을 사용하여 작업 순서를 결정하는 방식에 있습니다. React는 더 이상 경직된 하향식 하이드레이션 스크립트를 따르지 않고, 사용자에게 동적으로 반응합니다.

사용자가 최우선입니다

핵심 원칙은 다음과 같습니다: React는 사용자가 상호작용하는 컴포넌트의 하이드레이션을 우선시합니다.

React가 페이지를 하이드레이션하는 동안, 루트 레벨에 이벤트 리스너를 부착합니다. 만약 사용자가 아직 하이드레이션되지 않은 컴포넌트 내부의 버튼을 클릭하면, React는 매우 영리한 작업을 수행합니다:

  1. 이벤트 캡처: React는 루트에서 클릭 이벤트를 캡처합니다.
  2. 우선순위 지정: 사용자가 어떤 컴포넌트를 클릭했는지 식별합니다. 그런 다음 해당 특정 컴포넌트와 그 부모 컴포넌트의 하이드레이션 우선순위를 높입니다. 진행 중이던 낮은 우선순위의 하이드레이션 작업은 일시 중지됩니다.
  3. 하이드레이션 및 재실행: React는 대상 컴포넌트를 긴급하게 하이드레이션합니다. 하이드레이션이 완료되고 `onClick` 핸들러가 부착되면, React는 캡처된 클릭 이벤트를 재실행합니다.

사용자 관점에서는, 마치 컴포넌트가 처음부터 상호작용 가능했던 것처럼 상호작용이 그냥 작동합니다. 사용자는 즉각적인 반응을 위해 정교한 우선순위 지정 과정이 배후에서 일어났다는 사실을 전혀 인지하지 못합니다.

단계별 시나리오

이것이 실제로 어떻게 작동하는지 알아보기 위해 전자상거래 페이지 예시를 살펴보겠습니다. 이 페이지에는 메인 상품 그리드, 복잡한 필터가 있는 사이드바, 그리고 하단에 무거운 서드파티 채팅 위젯이 있습니다.

  1. 서버 스트리밍: 서버는 상품 그리드를 포함한 초기 HTML 뼈대를 보냅니다. 사이드바와 채팅 위젯은 ``로 감싸여 있으며, 그들의 폴백 UI(스켈레톤/로더)가 전송됩니다.
  2. 초기 렌더링: 브라우저는 상품 그리드를 렌더링합니다. 사용자는 거의 즉시 상품을 볼 수 있습니다. 아직 자바스크립트가 연결되지 않았기 때문에 TTI는 여전히 높습니다.
  3. 코드 로딩: 자바스크립트 번들이 다운로드되기 시작합니다. 사이드바와 채팅 위젯의 코드는 별도의 코드 분할된 청크에 있다고 가정해 봅시다.
  4. 사용자 상호작용: 어떤 것도 하이드레이션이 끝나기 전에, 사용자는 마음에 드는 상품을 보고 상품 그리드 내의 "장바구니에 담기" 버튼을 클릭합니다.
  5. 우선순위의 마법: React가 클릭을 캡처합니다. 클릭이 `ProductGrid` 컴포넌트 내에서 발생한 것을 확인합니다. 즉시 페이지의 다른 부분(방금 시작했을 수도 있는)의 하이드레이션을 중단하거나 일시 중지하고, 오직 `ProductGrid`의 하이드레이션에만 집중합니다.
  6. 빠른 상호작용성: `ProductGrid` 컴포넌트의 코드는 메인 번들에 있을 가능성이 높기 때문에 매우 빠르게 하이드레이션됩니다. `onClick` 핸들러가 부착되고, 캡처된 클릭 이벤트가 재실행됩니다. 상품이 장바구니에 추가됩니다. 사용자는 즉각적인 피드백을 받습니다.
  7. 하이드레이션 재개: 이제 높은 우선순위의 상호작용이 처리되었으므로, React는 작업을 재개합니다. 사이드바를 하이드레이션하기 시작합니다. 마지막으로, 채팅 위젯의 코드가 도착하면 해당 컴포넌트를 마지막으로 하이드레이션합니다.

결과는 어떨까요? 페이지의 가장 중요한 부분에 대한 TTI는 사용자의 의도에 따라 거의 즉각적이었습니다. 전체 페이지 TTI는 더 이상 하나의 무서운 숫자가 아니라, 점진적이고 사용자 중심적인 과정이 됩니다.

글로벌 사용자를 위한 실질적인 이점

극적으로 향상된 체감 성능

가장 중요한 이점은 사용자가 체감하는 성능의 엄청난 향상입니다. 사용자가 상호작용하는 페이지 부분을 먼저 사용 가능하게 만듦으로써, 애플리케이션이 더 빠르게 *느껴집니다*. 이것은 사용자 유지에 매우 중요합니다. 개발도상국의 느린 3G 네트워크를 사용하는 사용자에게, 전체 페이지가 상호작용 가능해지기까지 15초를 기다리는 것과 3초 만에 주요 콘텐츠와 상호작용할 수 있게 되는 것의 차이는 엄청납니다.

더 나은 코어 웹 바이탈

선택적 하이드레이션은 구글의 코어 웹 바이탈에 직접적인 영향을 미칩니다:

무거운 컴포넌트로부터 콘텐츠 분리

현대의 웹 앱은 분석, A/B 테스트, 고객 지원 채팅, 광고 등을 위한 무거운 서드파티 스크립트로 가득 차 있는 경우가 많습니다. 역사적으로 이러한 스크립트들은 전체 애플리케이션이 상호작용 가능해지는 것을 막을 수 있었습니다. 선택적 하이드레이션과 ``를 사용하면, 이러한 중요하지 않은 컴포넌트들을 완전히 분리할 수 있습니다. 주요 애플리케이션 콘텐츠는 로드되고 상호작용 가능해지는 동안, 이 무거운 스크립트들은 핵심 사용자 경험에 영향을 주지 않고 백그라운드에서 로드되고 하이드레이션될 수 있습니다.

더 회복력 있는 애플리케이션

하이드레이션이 청크 단위로 발생할 수 있기 때문에, 중요하지 않은 한 컴포넌트(예: 소셜 미디어 위젯)의 오류가 반드시 전체 페이지를 망가뜨리지는 않습니다. React는 잠재적으로 해당 `` 경계 내에서 오류를 격리시키고, 나머지 애플리케이션은 상호작용 가능한 상태로 유지할 수 있습니다.

실제 구현 및 모범 사례

선택적 하이드레이션을 채택하는 것은 복잡한 새 코드를 작성하는 것보다 애플리케이션을 올바르게 구조화하는 것에 더 가깝습니다. Next.js(앱 라우터 포함)나 Remix와 같은 최신 프레임워크는 대부분의 서버 설정을 대신 처리해주지만, 핵심 원리를 이해하는 것이 중요합니다.

`hydrateRoot` API 채택

클라이언트에서 이 새로운 동작의 진입점은 `hydrateRoot` API입니다. 기존의 `ReactDOM.hydrate`에서 `ReactDOM.hydrateRoot`로 전환해야 합니다.


// 이전 (레거시)
import { hydrate } from 'react-dom';
const container = document.getElementById('root');
hydrate(<App />, container);

// 이후 (React 18+)
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('root');
const root = hydrateRoot(container, <App />);

이 간단한 변경만으로 애플리케이션이 선택적 하이드레이션을 포함한 새로운 동시성 렌더링 기능을 사용하도록 설정됩니다.

``의 전략적 사용

선택적 하이드레이션의 힘은 `` 경계를 어떻게 배치하느냐에 따라 발휘됩니다. 모든 작은 컴포넌트를 감싸지 말고, 사용자 흐름을 방해하지 않고 독립적으로 로드될 수 있는 논리적인 UI 단위 또는 '섬' 단위로 생각하세요.

`` 경계에 적합한 대상은 다음과 같습니다:

코드 분할을 위해 `React.lazy`와 결합

선택적 하이드레이션은 `React.lazy`를 통한 코드 분할과 결합될 때 더욱 강력해집니다. 이는 우선순위가 낮은 컴포넌트의 자바스크립트가 필요할 때까지 다운로드되지 않도록 하여 초기 번들 크기를 더욱 줄여줍니다.


import React, { Suspense, lazy } from 'react';

const CommentsSection = lazy(() => import('./CommentsSection'));
const ChatWidget = lazy(() => import('./ChatWidget'));

function App() {
  return (
    <div>
      <ArticleContent />
      <Suspense fallback={<CommentsSkeleton />}>
        <CommentsSection />
      </Suspense>
      <Suspense fallback={null}> <!-- 숨겨진 위젯에는 시각적 로더가 필요 없음 -->
        <ChatWidget />
      </Suspense>
    </div>
  );
}

이 설정에서 `CommentsSection`과 `ChatWidget`의 자바스크립트 코드는 별도의 파일에 존재하게 됩니다. 브라우저는 React가 렌더링하기로 결정했을 때만 해당 파일들을 가져오며, 이들은 메인 `ArticleContent`를 막지 않고 독립적으로 하이드레이션됩니다.

`renderToPipeableStream`을 사용한 서버 사이드 설정

맞춤형 SSR 솔루션을 구축하는 경우, 사용해야 할 서버 사이드 API는 `renderToPipeableStream`입니다. 이 API는 스트리밍을 위해 특별히 설계되었으며 ``와 원활하게 통합됩니다. 이를 통해 HTML을 언제 보낼지, 오류를 어떻게 처리할지에 대한 세밀한 제어가 가능합니다. 하지만 대부분의 개발자에게는 이러한 복잡성을 추상화해주는 Next.js와 같은 메타 프레임워크가 권장되는 경로입니다.

미래: React 서버 컴포넌트

선택적 하이드레이션은 기념비적인 진전이지만, 훨씬 더 큰 이야기의 일부입니다. 다음 진화는 React 서버 컴포넌트(RSC)입니다. RSC는 서버에서만 독점적으로 실행되며 클라이언트로 자바스크립트를 전혀 보내지 않는 컴포넌트입니다. 이는 전혀 하이드레이션할 필요가 없음을 의미하며, 클라이언트 사이드 자바스크립트 번들을 훨씬 더 줄여줍니다.

선택적 하이드레이션과 RSC는 완벽하게 함께 작동합니다. 순수하게 데이터를 표시하기 위한 앱의 부분은 RSC(클라이언트 사이드 JS 없음)가 될 수 있고, 상호작용적인 부분은 선택적 하이드레이션의 이점을 누리는 클라이언트 컴포넌트가 될 수 있습니다. 이 조합은 React로 고성능의 인터랙티브 애플리케이션을 구축하는 미래를 대표합니다.

결론: 더 똑똑하게, 덜 힘들게 하이드레이션하기

React의 선택적 하이드레이션은 단순한 성능 최적화 그 이상입니다. 이는 더 사용자 중심적인 아키텍처를 향한 근본적인 전환입니다. 과거의 '전부 아니면 전무'라는 제약에서 벗어남으로써, React 18은 개발자들이 까다로운 네트워크 조건에서도 로드 속도가 빠를 뿐만 아니라 상호작용도 빠른 애플리케이션을 구축할 수 있도록 힘을 실어줍니다.

핵심 요점은 명확합니다:

글로벌 사용자를 위해 개발하는 개발자로서, 우리의 목표는 모든 사람에게 접근 가능하고, 회복력 있으며, 즐거운 경험을 만드는 것입니다. 선택적 하이드레이션의 힘을 받아들임으로써, 우리는 사용자를 기다리게 하는 것을 멈추고 우선순위가 지정된 컴포넌트 하나하나를 통해 그 약속을 이행하기 시작할 수 있습니다.