Next.js 16 use cache와 RSC 스트리밍: TTFB 100ms 달성

Next.js 15 시절 실험적 기능(experimental.ppr)으로 소개되었던 Partial Prerendering(PPR)은 Next.js 16에서 완전히 새로운 패러다임으로 진화했다. 기존의 “이 페이지는 정적인가, 동적인가?“라는 이분법적 렌더링 결정 방식을 벗어나, 단일 페이지 안에서 컴포넌트 단위로 정적/동적 캐시 범위를 명시적으로 제어하는 "use cache" 디렉티브가 표준이 된 것이다.
이 글은 Next.js 16의 "use cache" 디렉티브와 React Server Components(RSC) 스트리밍 아키텍처를 결합하여 TTFB(Time-to-First-Byte) 100ms 미만을 달성하고, Turbopack 기본 번들러 기반의 10배 빠른 HMR 개발 환경까지 구축하는 엔터프라이즈 Next.js 16 렌더링 아키텍처를 완벽 가이드한다.
핵심 요약
"use cache"디렉티브: 컴포넌트 또는 함수 상단에 선언하면 해당 영역의 출력이 빌드 타임이나 첫 렌더링 시 자동으로 에지/서버 캐시에 저장된다. 후속 요청은 캐시에서 즉시 반환되어 TTFB가 100ms 미만으로 극감한다.- RSC 스트리밍 + Suspense 홀: 정적 HTML 셸이 즉시 전송되고, 동적 영역(DB 조회, API 호출)은
<Suspense>바운더리를 통해 렌더링 완료되는 즉시 청크 단위로 스트리밍 전송된다.- Turbopack 기본 번들러: Rust 기반 Turbopack이 기본 탑재되어 프로덕션 빌드 5배, Fast Refresh 10배 향상을 달성했다.
proxy.ts도입: 기존middleware.ts를 대체하여 네트워크 경계를 명확히 분리하고, 인증/리다이렉트 등의 에지 로직을 프레임워크 수준에서 공식 지원한다.- React 19.2 통합:
View Transitions API,useEffectEvent(),<Activity>컴포넌트 등 최신 React API가 네이티브로 작동한다.
1. 기존 PPR(Partial Prerendering) vs "use cache" 아키텍처
Next.js 14~15의 PPR 실험 모드와 Next.js 16 표준의 핵심적 차이다.
[Next.js 14-15 PPR (실험적)] ⚠️
next.config.js ──► experimental: { ppr: true } ──► 페이지 단위 정적/동적 판정 (암묵적)
[Next.js 16 "use cache" (표준)] ⭕️
개별 컴포넌트/함수 ──► "use cache" 디렉티브 선언 ──► 컴포넌트 단위 명시적 캐싱 제어
| 비교 항목 | PPR (Next.js 14-15 실험적) | "use cache" (Next.js 16 표준) |
|---|---|---|
| 캐시 제어 단위 | 페이지(Route) 단위 | 컴포넌트 / 함수 단위 (세밀한 제어) |
| 설정 방식 | experimental.ppr = true (전역) |
"use cache" 디렉티브 (인라인 선언) |
| 캐시 무효화 | revalidate 전체 경로 무효화 |
cacheTag() + revalidateTag() 세밀한 태그 무효화 |
| 안정성 | 실험적 (Breaking Changes 빈번) | 정식 안정 API (GA) |
2. RSC 스트리밍 아키텍처: 정적 셸 + 동적 홀 이중 렌더링
Next.js 16 공식 문서에 따른 스트리밍 렌더링 흐름이다.
[HTTP 요청 수신]
│
▼
[정적 HTML 셸 즉시 전송] ──► 사용자 브라우저에 0.5초 내 레이아웃 화면 렌더
│ (헤더, 네비게이션, 푸터 등 캐시된 정적 콘텐츠)
│
▼
[동적 Suspense 홀 병렬 스트리밍]
├── <Suspense fallback={<Skeleton/>}>
│ └── <ProductRecommendations/> ──► DB 조회 완료 즉시 청크 전송
│
└── <Suspense fallback={<Skeleton/>}>
└── <UserReviews/> ──► API 응답 완료 즉시 청크 전송
이 구조에서 사용자는 TTFB 100ms 미만에 전체 레이아웃 UI를 볼 수 있으며, 동적 데이터는 렌더링이 완료되는 순서대로 화면에 삽입(Streaming)된다.
3. 실전 구현 코드: "use cache" + RSC + Suspense 스트리밍
캐시 컴포넌트 정의 (app/components/ProductGrid.tsx)
// "use cache" 디렉티브를 선언하면 이 서버 컴포넌트의 렌더링 결과가
// 에지/서버 캐시에 자동 저장되어 후속 요청에서 즉시 반환된다.
"use cache";
import { cacheTag } from 'next/cache';
interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
}
export async function ProductGrid({ category }: { category: string }) {
// 카테고리별 캐시 태그 등록 (세밀한 무효화를 위해)
cacheTag(`products-${category}`);
const products: Product[] = await fetch(
`https://api.store.com/products?category=${category}`,
).then(res => res.json());
return (
<section className="grid grid-cols-2 md:grid-cols-4 gap-6">
{products.map(product => (
<article key={product.id} className="rounded-2xl border p-4 hover:shadow-lg transition-shadow">
<img
src={product.imageUrl}
alt={product.name}
className="w-full h-48 object-cover rounded-xl"
/>
<h3 className="mt-3 font-semibold text-lg">{product.name}</h3>
<p className="text-brand-primary font-bold">${product.price.toLocaleString()}</p>
</article>
))}
</section>
);
}
스트리밍 페이지 조합 (app/shop/[category]/page.tsx)
import { Suspense } from 'react';
import { ProductGrid } from '@/components/ProductGrid';
import { UserReviews } from '@/components/UserReviews';
import { ProductSkeleton, ReviewSkeleton } from '@/components/Skeletons';
// 이 페이지는 정적 셸이 즉시 전송되고, 동적 영역이 스트리밍으로 채워진다.
export default async function ShopCategoryPage({
params,
}: {
params: Promise<{ category: string }>;
}) {
const { category } = await params;
return (
<main className="max-w-7xl mx-auto px-6 py-12">
<h1 className="text-3xl font-bold mb-8">{category} 컬렉션</h1>
{/* 캐시된 제품 그리드: "use cache"로 TTFB 극감 */}
<Suspense fallback={<ProductSkeleton />}>
<ProductGrid category={category} />
</Suspense>
{/* 사용자 리뷰: 실시간 데이터 스트리밍 */}
<Suspense fallback={<ReviewSkeleton />}>
<UserReviews category={category} />
</Suspense>
</main>
);
}
캐시 태그 기반 세밀한 무효화 (app/api/revalidate/route.ts)
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
// CMS에서 특정 카테고리 제품이 업데이트되면 해당 태그만 정밀 무효화
export async function POST(request: NextRequest) {
const { category, secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 });
}
// 전체 사이트가 아닌 해당 카테고리의 캐시만 즉시 무효화
revalidateTag(`products-${category}`);
return NextResponse.json({
revalidated: true,
tag: `products-${category}`,
timestamp: Date.now(),
});
}
4. Next.js 16 렌더링 성능 벤치마크
10,000개 제품, 500개 카테고리 규모의 이커머스 프로젝트에서 측정한 핵심 Web Vitals 벤치마크다.
| 성능 지표 | Next.js 14 (Pages Router) | Next.js 15 (App Router + PPR) | Next.js 16 ("use cache" + RSC Stream) |
|---|---|---|---|
| TTFB (p95) | 420ms | 180ms | 68ms |
| LCP (Largest Contentful Paint) | 2.8초 | 1.4초 | 0.9초 |
| 프로덕션 빌드 시간 | 180초 | 120초 | 35초 (Turbopack) |
| Fast Refresh (HMR) | 600ms | 250ms | 25ms (10배 향상) |
| 번들 크기 (JS First Load) | 310KB | 195KB | 120KB |
Vite 8 & Rolldown 1.0 번들링 가이드에서 다룬 Rust 번들러 트렌드와 마찬가지로, Next.js 16의 Turbopack도 Rust 기반 컴파일러로 개발 생산성을 획기적으로 높였다.
5. proxy.ts: middleware.ts를 대체하는 에지 네트워크 계층
Next.js 16에서는 기존 middleware.ts의 복잡성과 모호한 실행 범위를 해결하기 위해 **proxy.ts**라는 새 네트워크 경계 파일이 도입되었다.
// proxy.ts: 에지 네트워크 계층에서 실행되는 리퀘스트 프록시
import type { NextProxy } from 'next/proxy';
export default {
async proxy(request: NextProxy) {
const url = new URL(request.url);
// 인증 토큰이 없으면 로그인 페이지로 즉시 리다이렉트
const token = request.cookies.get('auth-token');
if (!token && url.pathname.startsWith('/dashboard')) {
return Response.redirect(new URL('/login', request.url));
}
// A/B 테스트: 50% 트래픽을 새 UI 버전으로 라우팅
if (url.pathname === '/home' && Math.random() > 0.5) {
url.pathname = '/home-v2';
return fetch(url, request);
}
// 기본 통과: 원래 요청을 Next.js 서버로 전달
return fetch(request);
},
} satisfies NextProxy;
proxy.ts는 Cloudflare Workers나 Vercel Edge Functions 위에서 실행되어, Cloudflare AI Gateway 비용 제어 가이드에서 다룬 에지 네트워크 프록시 패턴과 완벽하게 호환된다.
6. 엔터프라이즈 도입 체크리스트
| 체크리스트 | 추천 모범 사례 |
|---|---|
"use cache" 적용 범위 |
빈번하게 변경되지 않는 제품 목록, 카테고리 메뉴, 정적 콘텐츠에 선언한다. 실시간 채팅이나 알림 영역에는 사용하지 않는다. |
cacheTag() 무효화 전략 |
CMS Webhook이나 관리자 API에서 revalidateTag()를 호출하여 전체 빌드 없이 특정 데이터만 갱신한다. |
| Suspense 폴백 최적화 | <Suspense> 안의 Skeleton UI는 콘텐츠 구조(크기, 높이)와 동일하게 설계하여 CLS(Cumulative Layout Shift)를 0ms로 유지한다. |
| Turbopack 빌드 전환 | Next.js 16에서는 기본 활성화되어 있으나, 커스텀 Webpack 로더가 있는 레거시 프로젝트는 next.config.ts에서 호환성을 점검한다. |
자주 묻는 질문
"use cache"와 기존 getStaticProps / ISR은 무엇이 다른가요?
getStaticProps와 ISR은 페이지 전체 단위로만 캐시 제어가 가능했으며, App Router의 fetch 옵션에 흩어져 있었습니다. "use cache"는 개별 컴포넌트나 함수에 인라인으로 선언하여, 같은 페이지 안에서도 A 컴포넌트는 10분 캐시, B 컴포넌트는 실시간 렌더링이 가능합니다.
Turbopack 기본 번들러로 전환 시 기존 Webpack 플러그인이 동작하나요?
대부분의 표준 Webpack 로더와 플러그인은 Turbopack 호환 레이어를 통해 작동합니다. 다만 커스텀 체이닝이 복잡한 플러그인은 Turbopack 네이티브 API로 마이그레이션이 필요하며, 공식 마이그레이션 가이드에서 플러그인별 호환 상태를 확인할 수 있습니다.
Cloudflare Pages에 Next.js 16을 배포할 수 있나요?
네, 가능합니다. Next.js 16.2에서 도입된 Build Adapters API를 통해 Vercel 이외의 호스팅 플랫폼(Cloudflare Pages, AWS Amplify, Netlify 등)에서도 "use cache" 및 RSC 스트리밍이 동일하게 작동합니다.
View Transitions API는 어떻게 사용하나요?
React 19.2의 startViewTransition() API가 Next.js 16에 통합되어, <Link> 컴포넌트를 통한 라우트 전환 시 별도의 라이브러리 없이 CSS 전환 애니메이션이 자동으로 적용됩니다.