Next.js 교재 · 12편 · Streaming

Suspense와 스트리밍 — 부분 렌더로 빠른 응답

느린 한 영역 때문에 빠른 모든 영역이 같이 늦지 않게.

페이지가 헤더→차트→테이블 순서로 채워지는 스트리밍 컨셉 일러스트

대시보드 페이지를 만들었는데 — 헤더는 즉시 보이는데 차트가 3초나 걸린다. 옛 SSR 이면 차트 끝날 때까지 화면이 통째로 백지. 사용자는 3초 동안 빈 페이지를 본다.

Next.js 의 Streaming SSR 은 이 흐름을 깬다. 헤더는 즉시 HTML 로 보내고, 차트는 준비되는 대로 나중에 같은 응답에 이어 붙인다. 그 마법의 입구가 <Suspense>.

1. Suspense 의 기본 — fallback 으로 분리

가장 단순한 예. 한 페이지 안에서 빠른 부분과 느린 부분을 분리.

// app/dashboard/page.tsx (서버 컴포넌트) import { Suspense } from 'react'; import { StatsHeader } from './stats-header'; import { SlowChart } from './slow-chart'; export default function DashboardPage() { return ( <div> <h1>대시보드</h1> <StatsHeader /> {/* 빠름 — 즉시 렌더 */} <Suspense fallback={<ChartSkeleton />}> <SlowChart /> {/* 느림 — 데이터 fetch 끝까지 ChartSkeleton */} </Suspense> </div> ); }

사용자가 보는 순서:

  1. 0ms — 헤더·StatsHeader·ChartSkeleton 까지 HTML 도착. 차트 자리엔 스켈레톤.
  2. 3000ms — SlowChart 의 fetch 끝남. 진짜 차트 HTML 이 같은 응답에 이어붙어 도착. React 가 자동으로 스켈레톤을 진짜 차트로 교체.

핵심은 응답이 한 번에 끝나지 않는다는 것. Next 가 HTTP 응답을 "닫지 않고" 유지하다가 준비되는 컴포넌트를 chunked transfer 로 추가 전송. 사용자 입장에선 그냥 페이지가 점진적으로 채워지는 느낌.

2. 여러 Suspense — 각자 독립 로딩

여러 느린 부분을 각자 따로 보여줄 수 있다. 차트와 테이블이 둘 다 느리면 둘이 서로 안 기다린다.

<Suspense fallback={<ChartSkeleton />}> <SlowChart /> {/* 2초 후 도착 */} </Suspense> <Suspense fallback={<TableSkeleton />}> <SlowTable /> {/* 4초 후 도착 */} </Suspense>

2초쯤 차트가 채워지고, 4초쯤 테이블이 채워진다. 둘이 동시 시작이라 4초가 최댓값. 옛 방식이면 둘 합쳐 6초.

독립 vs 묶기 — 개별 Suspense 로 분리하면 각자 끝나는 대로 채워짐. 하나의 Suspense 로 감싸면 둘 다 끝날 때까지 같이 기다림. 의도에 맞게 선택. UX 우선은 독립, "함께 등장해야 자연스러운 영역" 은 묶기.

3. loading.tsx vs <Suspense> 차이

11편의 loading.tsx 가 결국 같은 메커니즘인데 — 두 가지 차이가 있다.

구분loading.tsx<Suspense> 직접
범위그 폴더 page 전체감싼 부분만
위치특수 파일 컨벤션코드 어디든
여러 개폴더당 1개한 페이지에 N개
사용 시점페이지 전환·초기 로드한 페이지 안 부분 로드

둘은 충돌하지 않는다. loading.tsx 는 첫 도착까지의 큰 그림, <Suspense> 는 그 안에서 부분 부분. 함께 쓰면 깊이감 있는 UX 가 가능.

4. 데이터 fetching 과 결합 — async + Suspense

Suspense 가 진짜로 멈추는 트리거는 async Server Component 안의 await. 그 await 가 끝나기 전엔 그 컴포넌트 위치에 fallback 이 표시된다.

// app/dashboard/slow-chart.tsx (서버 컴포넌트) import { fetchChartData } from '@/lib/api'; export async function SlowChart() { const data = await fetchChartData(); // 2초 걸리는 fetch return <Chart data={data} />; }

이 컴포넌트가 <Suspense> 안에 있으면 await 동안 fallback. 끝나면 진짜 결과. useState·useEffect 없이 자연스럽게 작동한다.

5. 흔한 함정과 베스트 프랙티스

가장 흔한 실수 — Suspense 안의 컴포넌트가 props 로 Promise 를 받는 경우. 부모에서 데이터를 fetch 해 props 로 내려보내면 부모가 await 하느라 fallback 트리거가 안 된다. Suspense 안쪽 컴포넌트가 직접 await 해야 한다. 또는 React 19 의 use() 훅으로 promise 만 props 로 받기.

베스트 프랙티스 4가지:

  • 스켈레톤은 실제 UI 형태 모방 — 카드 자리에 비슷한 회색 블록. 모양이 크게 변하면 레이아웃 시프트(CLS) 가 발생해 SEO 점수 떨어짐.
  • 너무 잘게 쪼개지 말 것 — Suspense 가 5개 넘어가면 화면이 어수선. 보통 페이지당 2~3개가 sweet spot.
  • 위 영역 먼저 빠르게 — 사용자가 처음 보는 영역(헤더·hero)은 Suspense 밖에. 아래로 갈수록 느려도 OK.
  • error.tsx 와 함께 — Suspense 안에서 throw 가 일어나면 가장 가까운 error.tsx 가 잡는다. 한 영역만 깨지고 나머진 그대로.

요약 — 12편 좌표

여기까지 정리. Streaming SSR 의 입구는 <Suspense fallback={...}>. 안쪽 async 컴포넌트의 await 가 끝날 때까지 fallback, 끝나면 진짜 결과로 자동 교체. 여러 Suspense 는 각자 독립 로딩. loading.tsx 는 페이지 전체용, <Suspense> 는 부분용. 스켈레톤 모양 맞추기·너무 잘게 쪼개지 않기·위 영역 먼저. 다음 편에선 요청을 가로채는 — Middleware 를 본다.

다음 편 예고 — Middleware

요청 가로채기·리디렉트·인증 가드. 13편.

© 2026 주나이테크(주) @JUNAITECH