서버 컴포넌트 (page.tsx)
1// page.tsx (서버 컴포넌트)2import { Suspense } from 'react';3import { HydrationBoundary, dehydrate } from '@tanstack/react-query';4 5export default function StreamingPage() {6 return (7 <div>8 <h1>포스트 목록</h1>9 10 {/* Suspense로 감싸서 로딩 중에도 다른 UI 먼저 표시 */}11 <Suspense fallback={<LoadingSkeleton />}>12 <PostListWithData />13 </Suspense>14 </div>15 );16}17 18// 데이터를 가져오는 서버 컴포넌트19async function PostListWithData() {20 const queryClient = getQueryClient();21 await queryClient.prefetchQuery(postsQueryOptions());22 23 return (24 <HydrationBoundary state={dehydrate(queryClient)}>25 <PostList />26 </HydrationBoundary>27 );28}클라이언트 컴포넌트 (PostList.tsx)
1// PostList.tsx (클라이언트 컴포넌트)2'use client';3 4import { useSuspenseQuery } from '@tanstack/react-query';5 6export default function PostList() {7 // useSuspenseQuery: Suspense와 함께 사용8 // - 데이터가 없으면 Promise를 throw하여 Suspense fallback 표시9 // - 데이터가 있으면 바로 렌더링 (data가 undefined가 아님)10 const { data } = useSuspenseQuery({11 queryKey: ['posts'],12 queryFn: () => fetch('/api/posts').then(res => res.json()),13 });14 15 // data는 항상 존재 (undefined 체크 불필요)16 return (17 <ul>18 {data.posts.map(post => (19 <li key={post.id}>{post.title}</li>20 ))}21 </ul>22 );23}💡Streaming SSR 동작 원리
- 즉시 응답 시작: HTML 스트림 전송 시작
- Suspense fallback: 로딩 스켈레톤 먼저 전송
- 데이터 준비: 서버에서 데이터 fetch (병렬로)
- 청크 전송: 데이터 준비되면 실제 컨텐츠 스트리밍
- 점진적 Hydration: 각 청크별로 인터랙티브하게
실행 결과Streaming
스트리밍 중... (서버에서 데이터를 가져오는 동안 이 UI가 표시됩니다)
타이밍
페이지 렌더링 시작
헤더, 사이드바 등 즉시 표시
2026-05-23T17:32:35.329Z
데이터 스트리밍
Suspense fallback 표시 후 실제 데이터로 교체
✅useSuspenseQuery vs useQuery
useQuery
- • isLoading 직접 처리
- • data가 undefined일 수 있음
- • 수동 로딩 UI 구현
useSuspenseQuery
- • Suspense가 로딩 처리
- • data 항상 존재
- • 선언적 로딩 UI
⚠️Streaming의 장점
- TTFB 개선: 첫 번째 바이트 빠르게 전송
- 사용자 경험: 로딩 중에도 페이지 구조 표시
- 병렬 처리: 여러 데이터 소스 동시에 fetch
- 점진적 향상: 준비된 부분부터 인터랙티브