페이지 루트가 잘못된 단위였다

발행: (2026년 5월 23일 AM 12:37 GMT+9)
12 분 소요
원문: Dev.to

출처: Dev.to

오랫동안 React 서버 렌더링은 조용한 거래를 전제로 해왔습니다. 서버는 브라우저에 HTML을 먼저 전달해 사용자가 빈 페이지를 바라보는 일을 방지했습니다. 그리고 JavaScript가 로드되면 React가 다시 등장해 루트부터 해당 HTML을 차지했습니다.
이것은 구현 세부 사항처럼 들릴 수 있지만 실제로는 아키텍처적인 주장이었습니다. 즉, “페이지는 하나의 단위이며, 하나의 루트를 가지고, 하나의 프로그램처럼 인터랙티브해진다”는 것이죠.

몇몇 페이지는 이 모델에 충분히 부합합니다. 대시보드, 에디터, 혹은 복잡한 애플리케이션 셸은 보통 하나의 연결된 클라이언트‑사이드 시스템이 되기를 원합니다. 하지만 웹에는 그런 식으로 설계되지 않은 페이지가 넘쳐납니다.
예를 들어, 상품 페이지는 구매 박스, 리뷰, 추천, 배지, 미디어, 지도, 몇 개의 아코디언, 그리고 아무도 건드리지 않는 캐러셀 등을 포함합니다. 문서 페이지는 대부분 텍스트와 검색 박스만 있죠. 마케팅 페이지는 서버‑렌더링된 긴 콘텐츠와 여기저기 흩어진 몇 개의 인터랙션 포인트로 이루어져 있습니다. 같은 루트를 통해 모든 것을 하이드레이션하는 것은 프레임워크 입장에서는 편리하지만, 페이지 자체를 정확히 표현한다고 보긴 어렵습니다.

이것이 React 18 선택적 하이드레이션, TanStack Start 지연 하이드레이션, 그리고 @lazarv/react-server 하이드레이션 아일랜드를 연결하는 흐름입니다. 모두 같은 오래된 거래에 대한 반응이지만, 반응 방식은 서로 다릅니다.

React 18 선택적 하이드레이션

React 18 이전에는 SSR 흐름이 어색했습니다. 데이터 수집 → HTML 렌더링 → JavaScript 로드 → 트리 하이드레이션 순으로 진행됐으며, 각 단계는 전체 앱이 끝나야 다음 단계가 시작될 수 있었습니다.

  • 댓글 로드가 느리면 전체 쉘이 기다렸고,
  • 댓글 번들이 크면 네비게이션이 지연됐으며,
  • 하이드레이션 비용이 크면 이미 화면에 보이는 컨트롤조차도 다른 작업에 가로막히는 느낌을 받았습니다.

Suspense가 이 흐름을 바꾸었습니다. 서버가 Suspense 경계 안에서 HTML을 스트리밍하면, 페이지의 “준비된” 부분은 “느린” 부분을 기다릴 필요가 없어집니다. 클라이언트도 같은 경계를 통해 하이드레이션하면, 준비된 JavaScript가 다른 번들을 기다리지 않아도 됩니다. 사용자가 아직 하이드레이션되지 않은 경계 안을 클릭하면, React는 그 경계를 하이드레이션 라인 앞쪽으로 이동시킬 수 있습니다.

이제는 댓글 위젯이 늦게 도착해도 페이지 나머지는 바로 유용하게 사용할 수 있습니다. 사이드바는 포스트 뒤에 하이드레이션될 수 있고, 사용자 인터랙션에 따라 경계가 앞당겨질 수 있습니다. 하이드레이션이 루트에서 전체 트리를 한 번에 진행하는 단일 연속 흐름이 아니라는 점이 큰 변화입니다.

하지만 React는 페이지의 소유자를 바꾸지는 않았습니다. 단지 소유자가 작업을 스케줄링하는 방식을 바꾼 것이죠.

Suspense 경계는 무엇인가?

Suspense 경계는 하나의 React 루트 안에 존재하는 스케줄링 단위입니다. 이를 통해 React는 스트리밍, 일시정지, 재개, 우선순위 지정, 그리고 서버 HTML을 보존하면서도 코드를 로드할 수 있습니다.
하지만 “이 부분은 이제 React의 문제가 아니다”는 의미는 아닙니다. 경계가 서버‑렌더링된 앱의 일부라면, React는 결국 이를 다시 조정(reconcile)해야 합니다. 부모의 상태나 컨텍스트가 바뀌어 보존된 HTML이 오래됐다고 판단되면, 일관성을 유지하기 위해 React가 개입합니다. 코드가 도착하고 경계가 의미가 있다면, 하이드레이션은 여전히 계획에 포함됩니다.

이 때문에 “문서의 일부에 대해 하이드레이션을 중단한다”는 오래된 React WG 질문이 중요한 전환점이 되었습니다. 제안된 트릭은 “경계를 영원히 suspend 시켜서 초기 클라이언트 작업에 정적 JSX‑무거운 콘텐츠가 들어가지 않게 하는 것”이었습니다. 답변은 기본적으로 “그것은 안정적인 소유 경계가 아니다. 업데이트가 여전히 React에게 검사를 요구하고, 컨텍스트가 여전히 영향을 미치며, 코드를 이미 다운로드했을 수도 있다”는 것이었습니다. 실제 방향은 Server Components였다고 합니다.

TanStack Start 지연 하이드레이션

TanStack Start의 접근 방식은 위의 근본적인 전환 직전 단계에 있습니다. 단일 루트 앱 모델을 그대로 받아들이면서도, 개발자가 특정 서버‑렌더링 서브트리를 초기 하이드레이션 큐에서 제외하고, 필요할 때만 받아들일 수 있게 합니다.

import { Hydrate } from "@tanstack/react-start";
import { visible } from "@tanstack/react-start/hydration";

export function ProductPage() {
  return (
    {/* ... */}
  );
}

위 예시에서 중요한 점은 리뷰가 lazy 로딩되는 것이 아니라 HTML에 그대로 존재한다는 것입니다. 서버가 이미 렌더링했으므로 사용자는 읽을 수 있고, 크롤러는 인덱싱할 수 있으며, CSS도 적용됩니다. 달라지는 것은 클라이언트 트리가 첫 번째 패스에서 해당 경계를 하이드레이션할 필요가 없다는 것입니다. 경계는 가시성, 유휴 시간, 사용자 인터랙션, 미디어 쿼리, 특정 조건 등에 따라 하이드레이션을 미룰 수 있고, never()를 사용해 초기 문서에서는 영구히 정적 상태로 남길 수도 있습니다.

React 선택적 하이드레이션이 존재하는 하이드레이션 작업의 순서를 결정한다면, TanStack Start는 그 작업 자체를 초기 큐에 넣을지 말지를 결정합니다. 컴파일러는 경계의 자식을 지연 청크로 분리해, 브라우저가 해당 JavaScript을 경계가 실제 하이드레이션될 때까지 다운로드하지 않게 할 수 있습니다. 이는 CPU 타이밍과 네트워크 타이밍 모두에 영향을 줍니다.

미묘한 점은 서버 HTML이 로딩 플레이스홀더가 아니라 실제 사용자에게 보여지는 콘텐츠라는 것입니다. TanStack Start에서 <Suspense fallback={...}>이미 서버 HTML이 존재하는 경우 초기 문서 하이드레이션 중에 보여지는 스켈레톤이 아닙니다.

첫 페이지 로드 시 <Suspense>가 문서에 이미 렌더링돼 있다면, 사용자는 계속해서 렌더링된 리뷰를 보게 됩니다. 스켈레톤은 다른 상황—앱이 이미 실행 중이고, 클라이언트‑사이드 내비게이션이나 조건부 렌더링을 통해 경계가 처음 등장했으며, 해당 경계에 보존된 서버 HTML이 없을 때—에 사용됩니다. 같은 fallback prop이 시작 이후의 클라이언트 세계를 위해 쓰이는 것이지, 초기 서버 세계를 대체하는 것이 아닙니다.

이 차이가 TanStack 모델의 특징을 만들어냅니다. 지연 하이드레이션은 “컴포넌트가 깨어날 때까지 스피너를 보여준다”는 것이 아니라, **“클라이언트가 해당 서브트리를 소유할 이유가 생길 때까지 서버‑렌더링된 결과를 보존한다”**는 의미입니다.

루트는 여전히 루트입니다. TanStack Start는 이를 명시적으로 강조합니다. 지연된

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.