본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 6장: Suspense 고급 패턴과 스트리밍 SSR
2026년 2월 2일·웹 개발·

6장: Suspense 고급 패턴과 스트리밍 SSR

React 19에서 강화된 Suspense의 고급 패턴, 스트리밍 SSR, 중첩 Suspense 전략, 배칭 동작, Partial Pre-rendering을 다룹니다.

16분630자8개 섹션
reactnextjsperformancefrontendtypescript
공유
react19-rsc6 / 11
1234567891011
이전5장: 새로운 훅 - useActionState, useFormStatus, useOptimistic다음7장: React Compiler - 자동 최적화의 시대

4장에서 use() API가 Suspense와 통합되는 방식을, 5장에서 새로운 훅들과 함께 사용하는 패턴을 살펴보았습니다. 이번 장에서는 Suspense 자체를 깊이 파고들어, React 19에서 달라진 동작, 고급 패턴, 스트리밍 SSR, 그리고 최신 기능인 Partial Pre-rendering까지 다룹니다.

React 19에서 Suspense의 변화

Suspense 히스토리

버전Suspense 기능
React 16.6React.lazy() 코드 스플리팅 전용
React 18데이터 페칭, 서버 사이드 스트리밍 지원
React 19즉시 커밋, 프리워밍, RSC 통합
React 19.2SSR 배칭, Partial Pre-rendering

즉시 커밋 (Immediate Commit)

React 18에서는 Suspense fallback이 표시될 때 약간의 지연이 있었습니다. 이는 짧은 네트워크 지연으로 인한 불필요한 로딩 UI 깜빡임을 방지하기 위한 의도였습니다. 하지만 이 동작이 예측하기 어렵고 오히려 사용자 경험을 해치는 경우가 있었습니다.

React 19에서는 Suspense fallback이 즉시 커밋됩니다. 컴포넌트가 suspend하면 곧바로 fallback이 표시됩니다.

즉시 커밋 동작
typescript
function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      {/* 이 컴포넌트가 suspend하면 PageSkeleton이 즉시 표시됨 */}
      <AsyncPage />
    </Suspense>
  );
}

프리워밍 (Pre-warming)

React 19에서는 컴포넌트가 suspend할 때, 해당 Suspense 경계의 형제(sibling) 컴포넌트들의 lazy 요청을 미리 시작합니다.

프리워밍 동작
typescript
function Dashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <SlowDataPanel />       {/* 이것이 suspend하면... */}
      <LazyChart />           {/* 이것의 코드 로딩을 미리 시작 */}
      <LazyNotifications />   {/* 이것의 코드 로딩도 미리 시작 */}
    </Suspense>
  );
}
 
const LazyChart = lazy(() => import('./Chart'));
const LazyNotifications = lazy(() => import('./Notifications'));

SlowDataPanel이 데이터를 기다리는 동안, React는 LazyChart와 LazyNotifications의 JavaScript 번들을 미리 다운로드합니다. 모든 준비가 완료되면 한 번에 표시됩니다.

Suspense 경계 설계 전략

안티패턴: 단일 거대 Suspense

나쁜 예: 전체를 하나의 Suspense로 감싸기
typescript
function Page() {
  return (
    <Suspense fallback={<FullPageSpinner />}>
      <Header />          {/* 빠르게 로드됨 */}
      <MainContent />     {/* 빠르게 로드됨 */}
      <SlowSidebar />     {/* 느리게 로드됨 */}
      <SlowComments />    {/* 느리게 로드됨 */}
    </Suspense>
  );
}

이 패턴에서는 SlowSidebar나 SlowComments 중 하나라도 로딩 중이면 전체 페이지가 스피너로 대체됩니다. Header와 MainContent가 이미 준비되어 있어도 표시되지 않습니다.

권장 패턴: 세분화된 Suspense 경계

좋은 예: 독립적인 Suspense 경계
typescript
function Page() {
  return (
    <div>
      {/* 핵심 콘텐츠는 외부 Suspense로 */}
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>
 
      <Suspense fallback={<ContentSkeleton />}>
        <MainContent />
      </Suspense>
 
      {/* 부가 콘텐츠는 별도 Suspense로 */}
      <aside>
        <Suspense fallback={<SidebarSkeleton />}>
          <SlowSidebar />
        </Suspense>
      </aside>
 
      <Suspense fallback={<CommentsSkeleton />}>
        <SlowComments />
      </Suspense>
    </div>
  );
}

각 섹션이 독립적으로 로드되므로, 빠르게 준비된 부분부터 순차적으로 표시됩니다.

중첩 Suspense 패턴

Suspense를 중첩하면 점진적 로딩(Progressive Loading) UI를 구현할 수 있습니다.

중첩 Suspense
typescript
function ArticlePage() {
  return (
    <Suspense fallback={<ArticlePageSkeleton />}>
      {/* 1단계: 전체 페이지 구조가 로드됨 */}
      <ArticleLayout>
        <ArticleHeader />
        <ArticleBody />
 
        {/* 2단계: 본문 로드 후 댓글 영역 로드 시작 */}
        <Suspense fallback={<CommentsSkeleton />}>
          <Comments />
 
          {/* 3단계: 댓글 로드 후 추천 글 로드 시작 */}
          <Suspense fallback={<RecommendationsSkeleton />}>
            <Recommendations />
          </Suspense>
        </Suspense>
      </ArticleLayout>
    </Suspense>
  );
}

사용자는 세 단계에 걸쳐 콘텐츠가 채워지는 것을 봅니다. 각 단계에서 해당 영역의 skeleton이 실제 콘텐츠로 교체됩니다.

스트리밍 SSR

전통적 SSR vs 스트리밍 SSR

전통적 SSR은 모든 데이터를 가져온 후 전체 HTML을 한 번에 전송합니다.

스트리밍 SSR은 준비된 부분부터 점진적으로 전송합니다.

Suspense와 스트리밍의 통합

Suspense 경계가 스트리밍의 단위가 됩니다. 각 Suspense 경계는 독립적인 HTML 청크로 전송됩니다.

스트리밍 SSR 구현
typescript
// app/articles/[id]/page.tsx
async function ArticlePage({ params }: { params: { id: string } }) {
  const article = await getArticle(params.id);
 
  return (
    <div>
      {/* 이 부분은 즉시 전송 */}
      <h1>{article.title}</h1>
      <p>{article.content}</p>
 
      {/* 이 부분은 데이터 준비되면 스트리밍 */}
      <Suspense fallback={<div>댓글 로딩 중...</div>}>
        <Comments articleId={params.id} />
      </Suspense>
 
      {/* 이 부분도 독립적으로 스트리밍 */}
      <Suspense fallback={<div>관련 글 로딩 중...</div>}>
        <RelatedArticles articleId={params.id} />
      </Suspense>
    </div>
  );
}
 
async function Comments({ articleId }: { articleId: string }) {
  // 이 await가 완료되면 HTML 청크로 스트리밍됨
  const comments = await getComments(articleId);
  return (
    <ul>
      {comments.map(c => <li key={c.id}>{c.text}</li>)}
    </ul>
  );
}

선택적 하이드레이션 (Selective Hydration)

스트리밍 SSR에서는 하이드레이션도 점진적으로 수행됩니다.

선택적 하이드레이션 동작
typescript
function Page() {
  return (
    <div>
      {/* 1순위: 사용자가 상호작용하는 영역을 먼저 하이드레이션 */}
      <Suspense fallback={<NavSkeleton />}>
        <Navigation />
      </Suspense>
 
      <main>
        {/* 2순위: 메인 콘텐츠 */}
        <Suspense fallback={<ContentSkeleton />}>
          <Content />
        </Suspense>
 
        {/* 3순위: 하단 영역은 나중에 하이드레이션 */}
        <Suspense fallback={<FooterSkeleton />}>
          <InteractiveFooter />
        </Suspense>
      </main>
    </div>
  );
}

사용자가 아직 하이드레이션되지 않은 영역을 클릭하면, React는 해당 영역의 하이드레이션을 우선적으로 처리합니다.

React 19.2: Suspense 배칭

React 19.2에서는 서버 렌더링된 Suspense 경계들이 비슷한 시점에 해결되면 함께 드러나는(reveal) 배칭 동작이 추가되었습니다.

Suspense 배칭 동작
typescript
function Dashboard() {
  return (
    <div className="grid grid-cols-3 gap-4">
      <Suspense fallback={<CardSkeleton />}>
        <StatsCard />     {/* 200ms 후 준비 */}
      </Suspense>
 
      <Suspense fallback={<CardSkeleton />}>
        <ChartCard />     {/* 220ms 후 준비 */}
      </Suspense>
 
      <Suspense fallback={<CardSkeleton />}>
        <ActivityCard />  {/* 250ms 후 준비 */}
      </Suspense>
    </div>
  );
}

세 카드가 비슷한 시점(200~250ms)에 준비되면, React는 이들을 모아서 한 번에 표시합니다. 하나씩 따로따로 나타나는 것보다 시각적으로 안정적입니다.

Info

배칭에는 LCP(Largest Contentful Paint) 보호 휴리스틱이 포함되어 있습니다. 페이지 로딩이 2.5초에 근접하면 배칭을 중단하고 준비된 콘텐츠부터 표시합니다. 이는 Core Web Vitals 지표를 보호하기 위한 장치입니다.

로딩 UI 패턴

Skeleton 컴포넌트 설계

재사용 가능한 Skeleton
typescript
function Skeleton({ className }: { className?: string }) {
  return (
    <div
      className={cn(
        'animate-pulse rounded bg-gray-200 dark:bg-gray-700',
        className
      )}
    />
  );
}
 
function ArticleSkeleton() {
  return (
    <div className="space-y-4">
      <Skeleton className="h-8 w-3/4" />      {/* 제목 */}
      <Skeleton className="h-4 w-1/4" />      {/* 날짜 */}
      <div className="space-y-2">
        <Skeleton className="h-4 w-full" />    {/* 본문 줄 */}
        <Skeleton className="h-4 w-full" />
        <Skeleton className="h-4 w-2/3" />
      </div>
    </div>
  );
}
 
function CommentsSkeleton() {
  return (
    <div className="space-y-3">
      {Array.from({ length: 3 }).map((_, i) => (
        <div key={i} className="flex gap-3">
          <Skeleton className="h-10 w-10 rounded-full" />
          <div className="flex-1 space-y-2">
            <Skeleton className="h-4 w-1/4" />
            <Skeleton className="h-4 w-3/4" />
          </div>
        </div>
      ))}
    </div>
  );
}

Next.js loading.tsx 패턴

Next.js App Router에서는 loading.tsx 파일이 자동으로 Suspense 경계를 생성합니다.

app/articles/loading.tsx
typescript
function ArticlesLoading() {
  return (
    <div className="space-y-6">
      <h1 className="text-2xl font-bold">글 목록</h1>
      <div className="grid gap-4">
        {Array.from({ length: 6 }).map((_, i) => (
          <ArticleSkeleton key={i} />
        ))}
      </div>
    </div>
  );
}
 
export default ArticlesLoading;

이 파일은 내부적으로 다음과 동일하게 동작합니다.

동등한 코드
typescript
<Suspense fallback={<ArticlesLoading />}>
  <ArticlesPage />
</Suspense>

Partial Pre-rendering (PPR)

React 19.2에서 도입된 Partial Pre-rendering은 정적 셸과 동적 콘텐츠를 분리하여 최적의 성능을 달성하는 기법입니다.

PPR의 동작 원리

  1. 빌드 타임: 정적 콘텐츠(헤더, 네비게이션, 레이아웃)를 미리 렌더링합니다.
  2. 요청 시점: Suspense 경계 내의 동적 콘텐츠만 서버에서 실행합니다.
  3. 응답: 정적 셸은 CDN에서 즉시 제공되고, 동적 부분은 스트리밍으로 채워집니다.
PPR 호환 페이지 구조
typescript
// 이 부분은 빌드 타임에 정적으로 렌더링됨
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
 
  return (
    <div>
      {/* 정적 영역: CDN 캐시 가능 */}
      <header>
        <Navigation />
        <Breadcrumb product={product} />
      </header>
 
      <main>
        <h1>{product.name}</h1>
        <ProductImages images={product.images} />
        <ProductDescription description={product.description} />
 
        {/* 동적 영역: 요청 시점에 스트리밍 */}
        <Suspense fallback={<PriceSkeleton />}>
          <DynamicPrice productId={product.id} />
        </Suspense>
 
        <Suspense fallback={<StockSkeleton />}>
          <StockStatus productId={product.id} />
        </Suspense>
 
        <Suspense fallback={<ReviewsSkeleton />}>
          <RecentReviews productId={product.id} />
        </Suspense>
      </main>
    </div>
  );
}

PPR의 이점

지표전통적 SSR스트리밍 SSRPPR
TTFB느림 (모든 데이터 대기)빠름매우 빠름 (CDN)
FCP느림빠름매우 빠름
LCP보통보통빠름
TTI느림보통빠름
캐싱어려움어려움정적 부분 캐싱 가능

Suspense 디버깅

React DevTools 활용

React DevTools의 Profiler 탭에서 Suspense 경계의 fallback 표시 시간을 확인할 수 있습니다. 어떤 컴포넌트가 suspend하고 있는지, 얼마나 오래 fallback이 표시되는지를 파악하여 성능을 최적화합니다.

성능 추적 (React 19.2)

React 19.2에서는 Chrome DevTools의 Performance 탭에 React 전용 트랙이 추가되었습니다. 이 트랙에서 다음을 확인할 수 있습니다.

  • 각 컴포넌트의 렌더링 시간
  • Suspense 경계의 suspend/reveal 타이밍
  • 스케줄러 우선순위

핵심 요약

  • React 19에서 Suspense fallback은 즉시 커밋되며, 형제 컴포넌트의 lazy 요청을 미리 시작(프리워밍)합니다.
  • Suspense 경계를 세분화하면 각 영역이 독립적으로 로드되어 사용자 경험이 개선됩니다.
  • 스트리밍 SSR에서 Suspense 경계는 스트리밍의 단위가 되며, 선택적 하이드레이션으로 인터랙션 대기 시간을 줄입니다.
  • React 19.2의 Suspense 배칭은 비슷한 시점에 해결된 경계들을 함께 표시하고, LCP 보호 휴리스틱을 포함합니다.
  • Partial Pre-rendering(PPR)은 정적 셸을 CDN에서 즉시 제공하고 동적 콘텐츠만 스트리밍하여 최적의 성능을 달성합니다.

다음 장에서는 React Compiler의 동작 원리와 적용 방법을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#react#nextjs#performance#frontend#typescript

관련 글

웹 개발

7장: React Compiler - 자동 최적화의 시대

React Compiler의 동작 원리, HIR 기반 분석, 자동 메모이제이션, 설치와 설정, ESLint 통합, 실전 적용 전략을 다룹니다.

2026년 2월 4일·15분
웹 개발

5장: 새로운 훅 - useActionState, useFormStatus, useOptimistic

React 19의 새로운 훅 3종을 심층 분석합니다. 폼 상태 관리, 제출 상태 추적, 낙관적 UI 업데이트의 실전 패턴을 다룹니다.

2026년 1월 31일·16분
웹 개발

8장: ref 개선, 메타데이터, 리소스 로딩 API

React 19의 DX 개선사항을 다룹니다. ref를 일반 props로 전달하는 방법, 컴포넌트 내 메타데이터 태그, 리소스 프리로딩 API를 살펴봅니다.

2026년 2월 6일·16분
이전 글5장: 새로운 훅 - useActionState, useFormStatus, useOptimistic
다음 글7장: React Compiler - 자동 최적화의 시대

댓글

목차

약 16분 남음
  • React 19에서 Suspense의 변화
    • Suspense 히스토리
    • 즉시 커밋 (Immediate Commit)
    • 프리워밍 (Pre-warming)
  • Suspense 경계 설계 전략
    • 안티패턴: 단일 거대 Suspense
    • 권장 패턴: 세분화된 Suspense 경계
    • 중첩 Suspense 패턴
  • 스트리밍 SSR
    • 전통적 SSR vs 스트리밍 SSR
    • Suspense와 스트리밍의 통합
    • 선택적 하이드레이션 (Selective Hydration)
  • React 19.2: Suspense 배칭
  • 로딩 UI 패턴
    • Skeleton 컴포넌트 설계
    • Next.js loading.tsx 패턴
  • Partial Pre-rendering (PPR)
    • PPR의 동작 원리
    • PPR의 이점
  • Suspense 디버깅
    • React DevTools 활용
    • 성능 추적 (React 19.2)
  • 핵심 요약