지연 하이드레이션
지연 하이드레이션은 실험적 기능입니다.
초기 페이지 로드 시 TanStack Start는 브라우저가 유용한 HTML을 빠르게 표시할 수 있도록 페이지를 서버 렌더링합니다. 하이드레이션은 이 초기 HTML 문서를 대화형 앱으로 전환하는 클라이언트 측 작업입니다. JavaScript를 로드하고 실행하며, 컴포넌트를 실행하고, 이벤트 핸들러를 연결하며, 기존 DOM을 React에 다시 연결합니다.
지연 하이드레이션은 이 초기 문서 하이드레이션 작업에 적용됩니다. 앱이 이미 실행 중인 이후에는 후속 클라이언트 측 탐색이 클라이언트 앱을 통해 렌더링되며, TanStack Start가 보존할 초기 서버 HTML은 없습니다.
기본적으로 TanStack Start는 전체 문서를 하이드레이션합니다. 이는 일반적으로 가장 단순하고 안전한 동작이지만, 큰 페이지에서는 사용자가 당장 필요로 하지 않을 수 있는 페이지 부분의 JavaScript를 로드하고 하이드레이션하는 데 상당한 시작 시간이 소요될 수 있습니다.
지연 하이드레이션을 사용하면 페이지에서 선택한 부분을 "아직 대화형이 아님"으로 표시할 수 있습니다. 서버 HTML은 문서에 그대로 유지되지만, TanStack Start는 전략이 시점에 도달했다고 판단할 때까지 해당 경계를 하이드레이션하지 않습니다. 기본적으로 컴파일러는 경계의 자식도 별도의 JavaScript 청크로 이동하므로 브라우저가 해당 코드의 로드도 지연할 수 있습니다.
페이지의 일부가 즉시 표시되고, 스타일이 적용되며, 색인될 수 있어야 하지만 즉시 대화형일 필요는 없을 때 지연 하이드레이션을 사용하세요.
지연 경계 추가
@tanstack/react-start/hydration의 전략과 함께 Hydrate을 사용하세요:
import { Hydrate } from '@tanstack/react-start'
import { visible } from '@tanstack/react-start/hydration'
export function ProductPage() {
return (
<Hydrate when={visible({ rootMargin: '400px' })}>
<Reviews />
</Hydrate>
)
}
초기 서버 응답에서 Reviews은 여전히 HTML로 렌더링됩니다. 초기
클라이언트 하이드레이션 과정에서는 해당 HTML이 보존되지만 Reviews React
트리는 아직 하이드레이션되지 않습니다. 경계가 뷰포트의
400px 이내로 들어오면 TanStack Start가 지연된 자식 청크를 로드하고
경계를 하이드레이션합니다.
Hydrate은 초기 문서에 존재하는 서버 HTML만 보존합니다. 예를 들어
클라이언트 측 탐색 후 동일한 경계가 나중에 처음 마운트되면
보존할 서버 HTML이 없으므로 클라이언트에서 정상적으로 렌더링됩니다.
지연할 대상 선택
적절한 경계는 페이지, 제품의 우선순위, 실제 사용자 행동에 따라 달라집니다. TanStack Start는 페이지의 어느 부분을 안전하게 지연할 수 있는지 알 수 없습니다.
일반적으로 즉각적인 상호작용에 필요하지 않은 SSR 콘텐츠가 적합한 후보입니다:
- 스크롤 없이 보이는 영역 아래의 리뷰, 댓글, 제품 세부 정보, 관련 콘텐츠 또는 긴 마케팅 섹션입니다.
- 지도, 차트, 캐러셀, 동영상 플레이어, 편집기 또는 임베드와 같은 리치 위젯입니다.
- 필터, 미리보기 창 또는 상황별 도구처럼 사용자의 의도에 따라 활성화되는 패널입니다.
- 일치하는 미디어 쿼리에서만 필요한 UI입니다.
- 초기 문서에서 하이드레이션하지 않아야 하는 정적 서버 렌더링 콘텐츠입니다.
사용자에게 즉시 필요할 수 있는 페이지 요소는 적합하지 않습니다:
- 기본 탐색 메뉴, 라우트 크롬, 검색창 및 계정 컨트롤입니다.
- 스크롤 없이 보이는 영역의 폼, 장바구니 추가 버튼, 결제 작업 또는 동의 컨트롤입니다.
- 사용자가 즉시 클릭할 수 있는 LCP 또는 히어로 영역의 인터랙티브 부분입니다.
- 페이지가 표시되는 즉시 키보드로 사용할 수 있어야 하는 접근성 필수 컨트롤입니다.
- 앱 시작 직후 props, context 또는 공유 state가 즉시 업데이트되어야 하는 컴포넌트입니다.
각 경계를 측정합니다. 유용한 경계는 예상되는 상호작용이 늦다고 느껴지지 않으면서 시작 JavaScript 또는 하이드레이션 작업을 줄입니다.
Astro Islands와 비교
Astro는 정적 상태에서 시작하여 "무엇을 활성화해야 하는가?"라고 묻습니다. 각 답은 HTML에 삽입된 격리된 프레임워크 루트입니다. Island는 DOM을 공유하는 독립적인 런타임입니다.
TanStack Start는 완전히 인터랙티브한 상태에서 시작하여 "무엇을 미룰 수 있는가?"라고 묻습니다. 기본적으로 전체
문서는 하나의 React 트리로 하이드레이션되며, Hydrate 경계는
그 트리 내부의 게이트입니다. Context, state 및 event는 평소처럼 흐르며,
하이드레이션은 부모 우선으로 진행됩니다.
트리거 용어는 같지만 기반은 다릅니다. Astro는 런타임을 구성하고, Start는
하나의 런타임을 스케줄링합니다. 이것이 Start가 interaction(), condition() 및 intent
버블링을 제공하고 Astro가 멀티 프레임워크를 제공하는 이유입니다.
React 선택적 하이드레이션과 비교
React의 선택적 하이드레이션은 서버 렌더링된 경계가 하이드레이션되는 순서를 제어합니다. 지연 하이드레이션은 각 경계의 하이드레이션 여부와 시점을 제어합니다.
React가 스트리밍 SSR 페이지를 하이드레이션하면 서버 렌더링된 모든
<Suspense> 경계는 결국 하이드레이션됩니다. 선택적 하이드레이션은
순서만 결정합니다. 각 경계는 해당 코드가 도착하는 즉시 하이드레이션되며,
사용자가 경계 내부를 클릭하면 React는 해당 경계를 대기열 맨 앞으로
이동합니다. 서버가 렌더링한 내용에 따라 작업량은 고정되며, React는
반응성이 좋게 느껴지도록 작업을 스케줄링합니다.
지연 하이드레이션은 애초에 대기열에 포함되는 항목을 변경합니다. 하나의
Hydrate 경계는 조건을 지정합니다. 해당 조건은 visible(), idle(),
interaction(), media(), condition() 또는 never()이며,
해당 조건이 충족될 때까지 경계는 정적 서버 HTML로 유지됩니다.
기본적으로 자식 JavaScript도 별도의 청크로 이동하며,
브라우저는 경계가 하이드레이션되기 직전까지 이를 다운로드하지 않습니다. 해당
조건이 충족되지 않으면 경계는 하이드레이션되지 않으며 해당 코드도
가져오지 않습니다.
두 가지를 함께 사용할 수 있습니다. Hydrate 경계는 React가
하위 트리의 하이드레이션을 시작할지 여부와 시점을 결정합니다. 경계가 열리면 내부의 모든 요소는
(<Suspense> 경계 포함) React의 일반 하이드레이션
스케줄러로 다시 전달됩니다. 하이드레이션이 반드시 필요하고 React가 적절히
우선순위를 지정하도록 하려면 <Suspense>을 사용합니다. 하이드레이션이 전혀 필요하지 않을 수도
있다면 Hydrate을 사용합니다.
세 가지 결정 사항
각 Hydrate 경계에는 세 가지 성능 관련 결정 사항이 있습니다.
| 결정 사항 | 옵션 | 제어하는 항목 |
|---|---|---|
| 하이드레이션 | when | 보존된 서버 HTML이 상호작용 가능해지는 시점입니다. |
| 코드 분할 | split | 자식 요소를 생성된 지연 자식 청크로 이동할지 여부입니다. |
| 준비 | prefetch | when 전략이 자식을 하이드레이션하기 전에 작업을 시작할지 여부입니다. |
when: 경계를 하이드레이션할 시점 결정
when은 필수입니다. 일반적인 경우에는 전략 객체를 전달합니다.
<Hydrate when={visible()}>
<Reviews />
</Hydrate>
결정에 브라우저 전용 정보가 필요한 경우에는 함수를 전달합니다.
import { Hydrate } from '@tanstack/react-start'
import { interaction, visible } from '@tanstack/react-start/hydration'
export function RecommendationsBoundary() {
return (
<Hydrate
when={() =>
navigator.connection?.saveData
? interaction({ events: 'click' })
: visible()
}
>
<Recommendations />
</Hydrate>
)
}
함수 형식은 클라이언트에서만 평가되며 전략을 동기적으로 반환해야
합니다. 초기 서버 HTML을 의도적으로 정적 상태로
유지하려면 never()을 사용합니다.
split: 별도의 자식 청크를 생성할지 결정
기본적으로 Hydrate은 자식 요소를 생성된 자식 청크로 분할합니다.
<Hydrate when={visible()}>
<HeavyWidget />
</Hydrate>
이렇게 하면 하이드레이션 작업과 자식 JavaScript 로딩이 모두 지연됩니다.
자식 코드가 작거나 다른 곳에서 이미 필요하며
하이드레이션 작업만 지연하려면 split={false}을 설정합니다.
import { Hydrate } from '@tanstack/react-start'
import { idle } from '@tanstack/react-start/hydration'
export function SmallWidgetBoundary() {
return (
<Hydrate when={idle()} split={false}>
<SmallWidget />
</Hydrate>
)
}
prefetch: 하이드레이션 전에 로딩을 시작할지 결정
prefetch은 경계가 하이드레이션되기 전에 로딩을 시작합니다. 다음 두 가지 형식이 있습니다.
| 형식 | 예시 | 용도 |
|---|---|---|
| 프리페치 전략 | prefetch={idle()} | 하이드레이션 전에 생성된 자식 청크를 미리 로드합니다. |
| 절차적 프리페치 | prefetch={async (ctx) => { ... }} | 자식 청크와 데이터 또는 기타 비동기 리소스를 미리 로드합니다. |
두 형식 모두 작업을 일찍 시작하지만, 경계가 상호작용 가능해지는 시점은
변경하지 않습니다. 해당 시점은 여전히 when에서 제어합니다.
프리페치 전략은 간결한 선언형 형식입니다.
import { idle, interaction, visible } from '@tanstack/react-start/hydration'
<Hydrate when={interaction()} prefetch={idle()}>
<ProductRecommendations />
</Hydrate>
<Hydrate
when={interaction()}
prefetch={visible({ rootMargin: '1200px' })}
>
<RelatedProducts />
</Hydrate>
전략 형식의 prefetch은 경계가 하이드레이션되기 전에 생성된 자식 청크를
다운로드합니다. when이 확인될 때 브라우저에 이미 청크가
있을 수 있으므로 이후의 하이드레이션 트리거가 더 빠르게 느껴질 수 있습니다. 생성된 자식
청크는 split이 활성화된 경우에만 존재하므로 split={false}인 경우 TypeScript는 전략 형식의
prefetch을 허용하지 않습니다.
사용자 지정 작업이 필요하면 절차적 프리페치를 사용합니다.
import { useQueryClient } from '@tanstack/react-query'
import { Hydrate } from '@tanstack/react-start'
import { visible } from '@tanstack/react-start/hydration'
function DeferredReviews() {
const queryClient = useQueryClient()
return (
<Hydrate
when={visible()}
prefetch={async ({ preload }) => {
await preload()
await queryClient.prefetchQuery(reviewsQueryOptions)
}}
>
<Reviews />
</Hydrate>
)
}
절차적 프리페치는 split={false}에서도 작동합니다. 이 경우 preload()은
확인된 no-op이지만, 함수는 여전히 데이터나 기타
리소스를 준비할 수 있습니다.
일반적인 사용법
접힌 영역 아래의 SSR 콘텐츠 하이드레이션
import { Hydrate } from '@tanstack/react-start'
import { visible } from '@tanstack/react-start/hydration'
export function ProductPage() {
return (
<>
<ProductHero />
<BuyBox />
<Hydrate when={visible({ rootMargin: '800px' })}>
<Reviews />
</Hydrate>
</>
)
}
경계가 실제로 뷰포트에 진입하기 전에 하이드레이션되어야 한다면 양수인 rootMargin을 사용합니다.
필요하기 전에 자식 청크 다운로드
import { Hydrate } from '@tanstack/react-start'
import { idle, visible } from '@tanstack/react-start/hydration'
export function ReviewsBoundary() {
return (
<Hydrate when={visible({ rootMargin: '200px' })} prefetch={idle()}>
<Reviews />
</Hydrate>
)
}
경계가 뷰포트에 가까워질 때까지 상호작용할 수 없는 상태로 유지하면서도, 유휴 시간에는 자식 청크 로드를 시작합니다.
사용자 의도가 나타날 때까지 위젯을 비활성 상태로 유지
import { Hydrate } from '@tanstack/react-start'
import { interaction, visible } from '@tanstack/react-start/hydration'
export function RecommendationsBoundary() {
return (
<Hydrate
when={interaction({ events: ['focusin', 'click'] })}
prefetch={visible({ rootMargin: '1200px' })}
>
<RecommendationCarousel />
</Hydrate>
)
}
표시되어 있거나 가까이 있지만 사용자가 사용하려 할 때만 중요해지는 고비용 컨트롤에 유용합니다.
코드 분할 없이 하이드레이션 지연
import { Hydrate } from '@tanstack/react-start'
import { idle } from '@tanstack/react-start/hydration'
export function BadgeBoundary() {
return (
<Hydrate when={idle()} split={false}>
<SmallPersonalizedBadge />
</Hydrate>
)
}
JavaScript가 이미 시작 번들에 포함되어 있거나 별도의 자식 청크를 만들 가치가 없을 때 사용합니다.
초기 SSR HTML을 정적으로 유지
import { Hydrate } from '@tanstack/react-start'
import { never } from '@tanstack/react-start/hydration'
export function MarketingPage() {
return (
<Hydrate when={never()}>
<StaticTrustBadges />
</Hydrate>
)
}
never()은 기존 서버 HTML을 보존하며 초기 문서 하이드레이션 중에는 경계를
하이드레이션하지 않습니다. 이후 클라이언트 측 탐색 중에 같은 경계가 마운트되면,
보존할 초기 서버 HTML이 없으므로 정상적으로 렌더링됩니다. never()은
프리페치 전략으로 사용할 수 없습니다.
Hydrate props 재사용
Hydrate에 펼쳐 넣을 재사용 가능한 객체에는 HydrateOptions을 사용합니다:
import { Hydrate } from '@tanstack/react-start'
import type { HydrateOptions } from '@tanstack/react-start'
import { visible } from '@tanstack/react-start/hydration'
const belowFoldProps = {
when: () => visible({ rootMargin: '800px' }),
} satisfies HydrateOptions
export function Page() {
return (
<Hydrate
{...belowFoldProps}
prefetch={async ({ preload }) => {
await preload()
}}
>
<Widget />
</Hydrate>
)
}
인라인 when 및 prefetch 함수가 지원됩니다. 이를 useCallback으로 래핑할
필요는 없습니다. TanStack Start는 내부적으로 최신 콜백을 유지하며 함수 식별자가
변경되었다는 이유만으로 하이드레이션 리스너를 다시 등록하지 않습니다. 경계의 의미가
변경되면 일반 React key을 사용해 새 경계를 생성합니다.
Hydrate Props 참고 자료
Hydrate은 다음 props를 받습니다:
| Prop | 타입 | 참고 |
|---|---|---|
when | HydrationStrategy | () => HydrationStrategy | 필수입니다. 경계가 언제 하이드레이션되는지 제어합니다. 함수 형식은 클라이언트 전용이며 동기식입니다. |
prefetch | HydrationPrefetchStrategy | HydrationPrefetchFunction | 선택 사항입니다. 전략 형식은 분할된 자식 청크를 미리 로드합니다. 함수 형식은 청크, 데이터 또는 기타 리소스를 미리 로드할 수 있으며 split={false}과 함께 사용할 수 있습니다. |
split | boolean | 기본값은 true입니다. 리터럴 false로 설정하면 컴파일러 추출을 비활성화하고 하이드레이션 작업만 지연합니다. |
fallback | ReactNode | 앱이 이미 하이드레이션된 후 마운트되고 자식 청크 또는 자식 Suspense로 인해 일시 중단되는 경계를 위한 클라이언트 전용 로딩 UI입니다. |
onHydrated | () => void | 경계가 클라이언트에서 하이드레이션된 후 한 번 실행됩니다. |
전략 참조
@tanstack/react-start/hydration에서 전략을 가져옵니다.
| 전략 | 동작 |
|---|---|
load() | 앱이 하이드레이션되는 즉시 하이드레이션됩니다. |
idle() | requestIdleCallback에서 하이드레이션되며, 유휴 콜백을 사용할 수 없으면 timeout 후에 하이드레이션됩니다. |
visible() | 경계 마커가 뷰포트에 들어오면 하이드레이션됩니다. |
media() | 미디어 쿼리가 일치하면 하이드레이션됩니다. |
interaction() | 구성된 상호작용 의도 이벤트에서 하이드레이션됩니다. |
condition() | 조건이 참이 되면 하이드레이션됩니다. |
never() | 서버에서 처음 렌더링된 경계를 절대 하이드레이션하지 않습니다. |
전략 옵션:
| 전략 | 옵션 |
|---|---|
idle | { timeout?: number }이며, 기본값은 2000입니다. |
visible | { rootMargin?: string; threshold?: number | Array<number> }이며, 기본 여백은 600px입니다. |
media | 쿼리 문자열입니다(예: media('(min-width: 800px)')). |
interaction | { events?: supported event or readonly array of supported events }. |
condition | 불리언 또는 불리언을 반환하는 함수입니다. |
지원되는 상호작용 이벤트는 auxclick, click, contextmenu,
dblclick, focusin, keydown, keyup, mousedown, mouseenter,
mouseover, mouseup, pointerdown, pointerenter, pointerover 및
pointerup.
기본 interaction() 이벤트 목록은 pointerenter, focusin,
pointerdown 및 click입니다. 경계가 다음을 수신해야 하는 경우 events을 사용합니다.
다른 이벤트 또는 더 작은 이벤트 집합:
import { Hydrate } from '@tanstack/react-start'
import { interaction } from '@tanstack/react-start/hydration'
<Hydrate when={interaction({ events: 'dblclick' })}>
<PreviewEditor />
</Hydrate>
<Hydrate when={interaction({ events: ['contextmenu', 'dblclick'] })}>
<ContextMenuEditor />
</Hydrate>
condition() 경계가 하이드레이션되면 나중에 조건이
false가 되더라도 하이드레이션된 상태를 유지합니다:
import { Hydrate } from '@tanstack/react-start'
import { condition } from '@tanstack/react-start/hydration'
export function CartRecommendationsBoundary() {
return (
<Hydrate when={condition(isCartOpen)}>
<CartRecommendations />
</Hydrate>
)
}
프리페치 참조
절차적 프리페치는 컨텍스트 객체를 받습니다:
| 속성 | 의미 |
|---|---|
preload() | 컴파일러가 생성한 자식 청크를 로드합니다. split={false}이면 즉시 완료됩니다. |
waitFor(strategy) | 프리페치 전략, 하이드레이션 트리거 또는 중단을 기다립니다. |
signal | fetch과 같이 취소 가능한 비동기 작업을 위한 AbortSignal입니다. |
element | 사용자 정의 옵저버 또는 DOM 측정을 위한 경계 마커 요소입니다. |
waitFor(strategy)는 다음 값으로 처리됩니다:
| 결과 | 의미 |
|---|---|
'prefetch' | 제공된 프리페치 전략이 정상적으로 처리되었습니다. |
'hydrate' | 경계의 하이드레이션 트리거가 먼저 실행되었습니다. 이제 필요한 작업을 수행합니다. |
'abort' | 경계가 언마운트되었거나 프리페치 수명 주기가 중단되었습니다. |
절차적 프리페치에서 반환된 프로미스는 의미가 있습니다. 프리페치 함수가
완료되기 전에 when 전략이 처리되면 대기 중인 작업이
하이드레이션을 차단합니다:
<Hydrate
when={visible()}
prefetch={async ({ preload }) => {
await preload()
}}
>
<Widget />
</Hydrate>
실행 후 결과를 기다리지 않는 작업은 하이드레이션을 차단하지 않습니다:
<Hydrate
when={visible()}
prefetch={({ preload }) => {
void preload()
}}
>
<Widget />
</Hydrate>
이 차이를 의도적으로 활용합니다. 첫 번째 하이드레이션 렌더링에 리소스가 필요하면 대기합니다. 리소스가 유용한 사전 준비에 불과하면 실행 후 결과를 기다리지 않습니다.
폴백
fallback는 최초 서버 렌더링 HTML의 플레이스홀더가 아닙니다. 최초
페이지 로드 시 TanStack Start는 경계가 하이드레이션될 때까지 기존 서버 HTML을
그대로 유지합니다:
<Hydrate when={visible()} fallback={<ReviewsSkeleton />}>
<Reviews />
</Hydrate>
이 예시에서 Reviews가 최초 HTML 문서에 있었다면 사용자는
서버에서 렌더링된 리뷰를 보게 됩니다. 경계가 visible()를 기다리는 동안
ReviewsSkeleton는 표시되지 않습니다.
fallback는 앱이 이미 실행 중인 상태에서 경계가 처음 나타나고 해당 경계에
기존 서버 HTML이 없을 때 사용됩니다. 일반적인 예로는 클라이언트 측 탐색,
조건부 패널 표시 또는 최초 문서에 콘텐츠가 없었던 탭 열기가
있습니다. 이러한 경우 경계는 클라이언트에서 렌더링되며, 생성된 자식 청크
또는 자식 Suspense가 아직 로드 중인 동안 fallback가 표시될 수 있습니다.
never()을 사용하면 초기 서버 HTML은 정적으로 유지되며 fallback은 사용되지 않습니다.
컴파일러는 서버 번들에서 정적으로 확인 가능한 fallback prop을 제거합니다.
서버 빌드가 해당 UI를 제거할 수 있도록 fallback을 직접 전달하거나, 인라인 객체 스프레드 또는
일회성 const 객체 스프레드를 통해 전달하는 방식을 권장합니다.
정확성과 업데이트
지연 하이드레이션은 React의 초기 하이드레이션 작업을 위한 성능 힌트입니다. 경계 외부의 state, prop, context 또는 store 업데이트로 인해 게이트가 열리기 전에 React가 경계 내부를 조정해야 하는 경우, React는 해당 전략에서 일반적으로 허용하는 시점보다 일찍 지연 경계를 하이드레이션할 수 있습니다. 이를 통해 정확성을 유지하고 주변 앱이 변경된 후 오래된 서버 HTML이 표시되는 것을 방지합니다.
never()은 초기 문서 하이드레이션의 예외입니다. 이를 의도적으로
정적인 SSR HTML로 취급합니다. 부모 업데이트로 never() 경계가
상호작용 가능해질 것이라고 기대해서는 안 됩니다. 동일한 경계가 나중에 클라이언트 측
탐색 중 마운트되면 정상적으로 렌더링됩니다.
중첩 경계
중첩 경계는 부모부터 하이드레이션됩니다. 자식 경계는 조상 경계가
하이드레이션된 후에만 하이드레이션될 수 있습니다. 즉, visible, media, idle 또는 condition과 같은
비상호작용 자식 전략은 부모 경계가 아직 탈수 상태인 동안에는 실행될 수 없습니다.
예를 들어 제품 페이지에서는 전체 리뷰 섹션이 뷰포트 가까이에 올 때까지 지연하면서, 사용자가 상호작용할 때까지 더 무거운 리뷰 도구는 비활성 상태로 유지할 수 있습니다:
import { Hydrate } from '@tanstack/react-start'
import { interaction, visible } from '@tanstack/react-start/hydration'
export function ProductPage() {
return (
<>
<ProductHero />
<BuyBox />
<Hydrate when={visible({ rootMargin: '600px' })}>
<section aria-labelledby="reviews-heading">
<h2 id="reviews-heading">Reviews</h2>
<ReviewsSummary />
<ReviewsList />
<Hydrate when={interaction({ events: ['focusin', 'click'] })}>
<ReviewFilters />
</Hydrate>
<Hydrate when={interaction({ events: 'click' })}>
<WriteReviewForm />
</Hydrate>
</section>
</Hydrate>
</>
)
}
이 예시에서는 리뷰 근처로 스크롤하면 부모가 먼저 하이드레이션됩니다. 그 후에야 중첩된 상호작용 경계가 포커스 또는 클릭으로 하이드레이션될 수 있습니다.
조상 자체가 상호작용을 기다리는 경우, 상호작용 의도로 아직 해결되지 않은 조상 체인을 해결할 수도 있습니다:
<Hydrate when={interaction({ events: ['focusin', 'click'] })}>
<section aria-label="Review tools">
<ReviewSortSummary />
<Hydrate when={interaction({ events: 'click' })}>
<WriteReviewForm />
</Hydrate>
</section>
</Hydrate>
첫 번째 유의미한 의도가 WriteReviewForm 내부의 클릭인 경우 TanStack
Start는 아직 해결되지 않은 부모 체인을 하이드레이션한 다음 대상 경계에 동일한 유형의
이벤트를 다시 디스패치합니다. 포인터 좌표와 같은 네이티브 리스너 페이로드 세부 정보가
보존된다고 보장되지는 않습니다. 초기 하이드레이션 중에는 여전히 never() 조상이 우선하므로
그 아래의 자손은 상호작용할 수 없는 상태로 유지됩니다.
프리로딩과 CSS
변환된 Hydrate JavaScript 청크는 라우트와 함께 모듈 프리로드되지 않습니다.
prefetch이 없으면 분할 경계가 렌더링될 준비가 되었을 때 자식 청크가 로드됩니다.
클라이언트 측 탐색이나 다른 클라이언트 전용 마운트 중 해당 import가 중단되면
경계의 fallback이 표시됩니다.
분할, 지연 및 never() 경계에서 사용하는 CSS는 일치하는 라우트의 SSR HTML에
링크됩니다. 서버에서 렌더링된 HTML은 JavaScript가 실행되기 전에 해당 스타일이 필요할 수 있으므로
생성된 자식 JavaScript 청크와 함께 지연되지 않습니다. 이는 라우트 수준의 에셋 링크입니다. 라우트 모듈에
CSS를 import하는 지연 경계가 포함되어 있으면, 해당 경계가 조건부 렌더링 뒤에 숨겨져
특정 응답에 나타나지 않는 경우에도 그 스타일시트가 라우트에 링크될 수 있습니다.
추출 제한
컴파일러 기반 Hydrate 분할은 경계의 자식을 생성된 가상 모듈로 옮기고
지연 컴포넌트를 통해 렌더링하는 방식으로 작동합니다. 이를 통해 TanStack Start는
나중에 로드할 별도의 자식 청크를 얻지만, 컴파일러가 JSX를 안전하게 옮길 수
있어야 합니다.
분할하려는 컴포넌트를 Hydrate 바로 안에 유지합니다. 불투명한
children props 뒤에 숨기면 컴파일러가 사용 위치에서 해당 자식을 생성된
자식 청크로 정적으로 추출할 수 없습니다.
분할 경계는 @tanstack/react-start에서 정적으로 import한 Hydrate 컴포넌트를
사용해야 합니다. 해당 import의 이름 변경은 지원됩니다:
import { Hydrate as Deferred } from '@tanstack/react-start'
export function ProductPage() {
return (
<Deferred when={visible()}>
<Reviews />
</Deferred>
)
}
Hydrate을 다른 컴포넌트 변수에 할당하는 방식은 분할 대상으로 분석되지 않습니다:
import { Hydrate } from '@tanstack/react-start'
const Deferred = Hydrate
<Deferred when={visible()}>
<Reviews />
</Deferred>
import한 Hydrate 태그를 직접 렌더링하거나 import 이름 변경을 사용하고, 컴포넌트
간접 참조가 필요하면 split={false}을 설정합니다.
추출을 사용하지 않으려면 리터럴 prop split={false}을 사용합니다. split={shouldSplit}과
같은 동적 값으로는 컴파일 시점에 사용하지 않도록 설정할 수 없습니다.
다음 패턴은 분할할 수 없습니다:
| 패턴 | 거부되는 이유 | 대신 수행할 작업 |
|---|---|---|
| 함수 형태의 자식 | 컴파일러는 렌더 함수를 옮기면서 예상되는 호출 패턴을 유지할 수 없습니다. | split={false}을 사용하거나 렌더링된 UI를 자식 컴포넌트로 옮깁니다. |
| 추출된 JSX 내부에서 직접 호출되는 Hook | 해당 JSX를 옮기면 Hook이 실행되는 위치도 옮겨집니다. | Hook 호출을 경계 내부의 컴포넌트로 옮긴 다음 해당 컴포넌트를 렌더링합니다. |
this 캡처 | 추출된 함수 컴포넌트는 클래스 인스턴스 컨텍스트를 안전하게 유지할 수 없습니다. | UI를 함수 컴포넌트로 감싸거나 split={false}을 사용합니다. |
super 캡처 | 추출된 함수 컴포넌트는 상위 클래스 접근을 유지할 수 없습니다. | UI를 함수 컴포넌트로 감싸거나 split={false}을 사용합니다. |
useThing()이 생성된 컴포넌트로 옮겨지므로 이 코드는 실패합니다:
<Hydrate when={idle()}>
<p>{useThing()}</p>
</Hydrate>
대신 Hook을 컴포넌트로 옮깁니다:
function ThingText() {
const thing = useThing()
return <p>{thing}</p>
}
export function ProductPage() {
return (
<Hydrate when={idle()}>
<ThingText />
</Hydrate>
)
}
주변 컴포넌트에서 캡처한 값은 생성된 자식 컴포넌트로 전달할 수 있지만, 경계는 단순하게 유지합니다. 추출로 인해 데이터 흐름이 복잡해지기 시작하면 이름이 있는 자식 컴포넌트를 사용하고 로직을 그 안에 배치하는 편이 좋습니다.
fallback 제거는 의도적으로 보수적으로 처리됩니다. 서버 빌드는 직접 전달된
fallback UI, 인라인 객체 스프레드 fallback UI, 단일 사용 const 객체 스프레드
fallback UI를 제거할 수 있습니다. fallback props가 동적 스프레드나 공유 객체 뒤에
숨겨져 있으면 컴파일러가 이를 유지할 수 있습니다.
현재 재사용 가능한 when 및 prefetch 헬퍼를 추출할 수 있지만, 하위 코드
분할이 필요하다면 일반 래퍼 컴포넌트 뒤에 분할 경계를 숨기지 마세요.
래퍼는 런타임에 하이드레이션을 지연할 수 있지만, 컴파일러는 임의의
컴포넌트 간접 참조를 통해 호출 지점의 하위 요소를 별도 청크로 안정적으로 옮길 수 없습니다.