← 홈으로

Streaming SSR

Suspense를 활용해 데이터를 기다리는 동안 다른 UI를 먼저 보여주는 방식

서버 컴포넌트 (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 동작 원리

  1. 즉시 응답 시작: HTML 스트림 전송 시작
  2. Suspense fallback: 로딩 스켈레톤 먼저 전송
  3. 데이터 준비: 서버에서 데이터 fetch (병렬로)
  4. 청크 전송: 데이터 준비되면 실제 컨텐츠 스트리밍
  5. 점진적 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
  • 점진적 향상: 준비된 부분부터 인터랙티브