🌐 웹개발

Streaming SSR

Streaming SSR

서버 렌더링 결과를 스트리밍으로 전송. TTFB 개선.

📖 상세 설명

Streaming SSR은 서버에서 렌더링한 HTML을 청크(chunk) 단위로 점진적으로 전송하는 기술입니다. 전체 페이지가 완성될 때까지 기다리지 않고, 준비된 부분부터 클라이언트로 보내 TTFB(Time To First Byte)를 크게 개선합니다.

React 18의 renderToPipeableStream과 Suspense를 결합하면 Streaming SSR을 구현할 수 있습니다. 빠르게 렌더링되는 컴포넌트는 먼저 전송하고, 데이터 페칭이 필요한 컴포넌트는 fallback을 보여준 뒤 준비되면 교체합니다.

Next.js App Router에서는 loading.js 파일이나 Suspense boundary를 사용하여 자동으로 Streaming을 적용합니다. 사용자는 페이지 셸을 즉시 보고, 데이터가 로드되면 해당 영역이 채워지는 것을 경험합니다.

Streaming SSR은 특히 데이터베이스 쿼리나 외부 API 호출이 오래 걸리는 페이지에서 효과적입니다. 핵심 콘텐츠를 먼저 보여주고 부가 정보(댓글, 추천 등)는 나중에 로드하여 체감 성능을 향상시킵니다.

💻 코드 예제

// Next.js App Router - Streaming with Suspense
// app/post/[id]/page.tsx

import { Suspense } from 'react';

// 즉시 렌더링되는 부분
async function PostContent({ id }: { id: string }) {
  const post = await fetch(`https://api.example.com/posts/${id}`, {
    cache: 'no-store'
  }).then(r => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

// 느린 데이터 로딩
async function Comments({ postId }: { postId: string }) {
  // 의도적으로 느린 API
  const comments = await fetch(
    `https://api.example.com/posts/${postId}/comments`,
    { cache: 'no-store' }
  ).then(r => r.json());

  return (
    <section>
      <h2>댓글 ({comments.length})</h2>
      {comments.map(c => <Comment key={c.id} {...c} />)}
    </section>
  );
}

// 추천 게시물 (가장 느림)
async function RelatedPosts({ postId }: { postId: string }) {
  const related = await getRelatedPosts(postId);
  return <RelatedPostList posts={related} />;
}

// 메인 페이지 - Streaming 적용
export default async function PostPage({
  params
}: { params: { id: string } }) {
  return (
    <main>
      {/* 먼저 전송됨 */}
      <Suspense fallback={<PostSkeleton />}>
        <PostContent id={params.id} />
      </Suspense>

      {/* 두 번째로 전송됨 */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments postId={params.id} />
      </Suspense>

      {/* 마지막에 전송됨 */}
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedPosts postId={params.id} />
      </Suspense>
    </main>
  );
}

// loading.js - 라우트 레벨 로딩 UI
// app/post/[id]/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-3/4 mb-4" />
      <div className="h-4 bg-gray-200 rounded w-full mb-2" />
      <div className="h-4 bg-gray-200 rounded w-5/6" />
    </div>
  );
}

🗣️ 실무 대화 예시

기술 미팅에서:

"상품 상세 페이지 TTFB가 2초나 걸려요. Streaming SSR 적용하면 상품 정보는 200ms에 보여주고, 리뷰랑 추천 상품은 Suspense로 나중에 로드하면 체감 속도가 확 빨라집니다."

기술 면접에서:

"Streaming SSR의 작동 원리는요?" - "Transfer-Encoding: chunked로 HTML을 조각내어 전송합니다. Suspense boundary마다 placeholder를 보내고, 데이터가 준비되면 script 태그로 실제 콘텐츠를 주입합니다."

코드 리뷰에서:

"Suspense boundary를 너무 작게 쪼개면 오히려 성능이 나빠져요. 논리적으로 묶이는 컴포넌트들은 하나의 Suspense로 감싸고, 독립적으로 로드해도 되는 것만 분리하세요."

⚠️ 주의사항

🔗 관련 용어

SSR Suspense React Server Components Hydration Next.js

📚 더 배우기

📄 Next.js 공식 문서 - Loading UI and Streaming 📄 React - renderToPipeableStream 📄 web.dev - Streaming SSR with React