TimewareTimeware
블로그 목록으로
블로그

Next.js 15 서버 컴포넌트 성능 최적화: TTFB 350ms를 40ms로 낮춘 방법

Timeware 사이트에서 Next.js 15 App Router를 사용하면서 실제로 적용한 성능 최적화 패턴들. PPR, 스트리밍, Turbopack HMR까지. 데이터 기반 접근법을 공유합니다.

2026년 3월 5일Timeware Engineeringnextjsreactperformanceserver-componentsweb
Next.js 15 서버 컴포넌트 성능 최적화: TTFB 350ms를 40ms로 낮춘 방법

요약

Timeware 사이트에서 Next.js 15 App Router를 사용하면서 실제로 적용한 성능 최적화 패턴들. PPR, 스트리밍, Turbopack HMR까지. 데이터 기반 접근법을 공유합니다.

Next.js 15 서버 컴포넌트 성능 최적화: TTFB 350ms를 40ms로 낮춘 방법

Executive Summary - Topic: Next.js 15 App Router 프로덕션 성능 최적화 패턴 - Target: Next.js 개발자, 프론트엔드/풀스택 엔지니어, 웹 성능 담당자 - TL;DR 1: 스트리밍 RSC로 TTFB를 350ms에서 40~90ms로 낮출 수 있다 - TL;DR 2: "use client"를 리프 컴포넌트에만 적용하면 번들 크기가 줄어든다 - TL;DR 3: Turbopack은 HMR 속도를 밀리초 단위로 만들어 개발 경험을 바꾼다

Timeware 사이트는 Next.js 15 App Router로 구동됩니다. 처음 도입했을 때 성능이 "좋은 것 같다"는 느낌은 있었는데, 실제로 측정해보니 생각과 달랐습니다.

RSC(React Server Components)를 잘못 쓰면 SSR보다 느릴 수 있고, 데이터 페칭 위치를 잘못 설정하면 폭포수(waterfall) 패턴이 생깁니다. 이 글은 실제로 측정하고, 수정하고, 다시 측정한 결과를 공유합니다.

1. 기준선 측정: 최적화 전 실제 수치

최적화 전 데이터 대시보드 페이지 성능:

code
1TTFB (Time to First Byte): 350~550ms (p50)
2FCP (First Contentful Paint): 1.2s
3LCP (Largest Contentful Paint): 2.8s
4Bundle size (JS): 486KB (gzipped)

Core Web Vitals 기준:

  • TTFB: 빨강 (>600ms 경고, >1800ms 나쁨 — 이 기준에서는 괜찮지만 개선 여지 있음)
  • LCP: 주의 (2.5s 이하가 좋음, 2.5~4s 개선 필요)

원인 분석:

  1. 데이터 페칭이 직렬로 실행됨 (A 완료 후 B 실행)
  2. 클라이언트 컴포넌트를 불필요하게 많이 사용
  3. PPR이 설정되지 않아 전체 페이지가 동적으로 렌더링

2. 병렬 데이터 페칭: 직렬 → 병렬

가장 쉽고 효과가 큰 개선입니다.

typescript
1// ❌ 직렬 페칭 (A 완료 후 B 실행)
2async function DashboardPage() {
3 const user = await getUser(); // 300ms
4 const orders = await getOrders(); // 250ms
5 const analytics = await getAnalytics(); // 200ms
6 // 총 750ms 소요
7
8 return <Dashboard user={user} orders={orders} analytics={analytics} />;
9}
10
11// ✅ 병렬 페칭 (모두 동시 실행)
12async function DashboardPage() {
13 const [user, orders, analytics] = await Promise.all([
14 getUser(), // 300ms
15 getOrders(), // 250ms
16 getAnalytics(), // 200ms
17 ]);
18 // 총 300ms 소요 (가장 느린 것 기준)
19
20 return <Dashboard user={user} orders={orders} analytics={analytics} />;
21}

이것만으로 데이터 페칭 시간이 750ms → 300ms로 줄었습니다.

3. 스트리밍 RSC: TTFB 350ms → 40~90ms

병렬 페칭을 해도 모든 데이터가 준비될 때까지 기다려야 합니다. 스트리밍을 쓰면 준비된 데이터부터 먼저 보낼 수 있습니다.

typescript
1// app/dashboard/page.tsx
2import { Suspense } from 'react';
3
4export default function DashboardPage() {
5 return (
6 <div>
7 {/* 정적 헤더: 즉시 전송 */}
8 <DashboardHeader />
9
10 {/* 빠른 데이터: 300ms 후 전송 */}
11 <Suspense fallback={<UserSkeleton />}>
12 <UserSection />
13 </Suspense>
14
15 {/* 느린 데이터: 준비되면 그때 전송 */}
16 <Suspense fallback={<AnalyticsSkeleton />}>
17 <AnalyticsSection />
18 </Suspense>
19 </div>
20 );
21}
22
23// 각 섹션이 독립적으로 데이터를 페칭
24async function AnalyticsSection() {
25 const analytics = await getAnalytics(); // 이 섹션만의 독립적 페칭
26 return <AnalyticsChart data={analytics} />;
27}

결과: TTFB가 40~90ms로 개선됩니다. 사용자는 헤더를 먼저 보고, 빠른 섹션이 로드되고, 느린 섹션이 마지막에 채워집니다.

4. "use client" 최소화: 번들 크기 43% 감소

Next.js App Router에서 가장 흔한 실수: 필요하지 않은 곳에 "use client"를 붙이는 것.

typescript
1// ❌ 잘못된 패턴: 전체 카드가 클라이언트 컴포넌트
2"use client";
3
4import { useState } from 'react';
5
6export function ProductCard({ product, reviews }) {
7 const [liked, setLiked] = useState(false);
8
9 return (
10 <div>
11 <h2>{product.name}</h2> {/* 정적 — 서버에서 렌더링해도 됨 */}
12 <p>{product.description}</p> {/* 정적 — 서버에서 렌더링해도 됨 */}
13 <ReviewList reviews={reviews} /> {/* 정적 — 서버에서 렌더링해도 됨 */}
14 <LikeButton liked={liked} onChange={setLiked} /> {/* 동적 — 클라이언트 필요 */}
15 </div>
16 );
17}
typescript
1// ✅ 올바른 패턴: 서버와 클라이언트 책임 분리
2// ProductCard.tsx — 서버 컴포넌트 (기본값)
3export function ProductCard({ product, reviews }) {
4 return (
5 <div>
6 <h2>{product.name}</h2>
7 <p>{product.description}</p>
8 <ReviewList reviews={reviews} />
9 <LikeButton productId={product.id} /> {/* 클라이언트 컴포넌트만 'use client' */}
10 </div>
11 );
12}
13
14// LikeButton.tsx — 클라이언트 컴포넌트
15"use client";
16import { useState } from 'react';
17
18export function LikeButton({ productId }: { productId: string }) {
19 const [liked, setLiked] = useState(false);
20 return (
21 <button onClick={() => setLiked(!liked)}>
22 {liked ? '❤️' : '🤍'}
23 </button>
24 );
25}

번들 크기 변화: 486KB → 276KB (43% 감소)

규칙: "use client"는 실제로 브라우저 API(useState, useEffect, window, document)가 필요한 컴포넌트에만 붙입니다.

5. PPR(Partial Prerendering): 정적 + 동적의 최선 조합

PPR은 Next.js 15의 실험적 기능으로, 페이지의 정적 셸을 엣지에서 즉시 제공하면서 동적 콘텐츠는 스트리밍으로 채웁니다.

typescript
1// next.config.mjs
2export default {
3 experimental: {
4 ppr: 'incremental', // 페이지별로 선택적 적용 가능
5 },
6};
7
8// app/blog/[slug]/page.tsx
9export const experimental_ppr = true; // 이 페이지에 PPR 활성화
10
11export default async function BlogPost({ params }) {
12 return (
13 <article>
14 {/* 정적: 즉시 제공 (엣지 캐시) */}
15 <BlogHeader slug={params.slug} />
16
17 {/* 정적: 즉시 제공 */}
18 <BlogContent slug={params.slug} />
19
20 {/* 동적: 스트리밍으로 채움 */}
21 <Suspense fallback={<CommentsSkeleton />}>
22 <Comments slug={params.slug} />
23 </Suspense>
24
25 {/* 동적: 사용자별 개인화 */}
26 <Suspense fallback={null}>
27 <RelatedPosts userId={await getUserId()} />
28 </Suspense>
29 </article>
30 );
31}

주의: PPR은 Next.js 15 기준 실험적(experimental) 기능입니다. 프로덕션 적용 전 철저한 테스트가 필요합니다.

6. Turbopack: 개발 경험의 실질적 변화

Turbopack은 Webpack의 Rust 기반 대체재로, Next.js 15에서 기본 개발 번들러로 사용됩니다.

실제 측정 수치 (Timeware 프로젝트 기준):

지표WebpackTurbopack개선
Cold start~8.2s~0.9s9배 빠름
HMR (컴포넌트 수정)~800ms~50ms16배 빠름
Initial compile~24s~4s6배 빠름

체감 변화: 컴포넌트를 수정하고 저장하면 거의 즉시 브라우저에 반영됩니다. 800ms HMR 지연이 50ms가 되면 개발 리듬이 달라집니다.

Turbopack 활성화:

bash
1# package.json
2{
3 "scripts": {
4 "dev": "next dev --turbopack" # Next.js 15.1부터 기본값
5 }
6}

7. 최종 결과

4가지 최적화를 모두 적용한 후:

code
1[최적화 전]
2TTFB: 350~550ms
3FCP: 1.2s
4LCP: 2.8s
5Bundle: 486KB
6
7[최적화 후]
8TTFB: 40~90ms ← 스트리밍 RSC
9FCP: 0.4s ← PPR + 정적 셸
10LCP: 1.1s ← 이미지 최적화 + LCP 우선 로딩
11Bundle: 276KB ← use client 최소화

Timeware 사이트 적용 패턴 정리

typescript
1// 저희가 사용하는 패턴 요약
2
3// 1. 데이터 페칭은 항상 병렬
4const [posts, meta] = await Promise.all([
5 fetchBlogPosts(),
6 fetchSiteMeta(),
7]);
8
9// 2. 느린 섹션은 Suspense로 감싸기
10<Suspense fallback={<PostListSkeleton />}>
11 <PostList />
12</Suspense>
13
14// 3. use client는 리프 컴포넌트에만
15// 4. 이미지는 next/image + priority 설정
16<Image src={hero} alt={alt} priority={isAboveFold} />
17
18// 5. Font는 next/font/local로 최적화
19import localFont from 'next/font/local';
20const pretendard = localFont({
21 src: './Pretendard-Variable.woff2',
22 display: 'swap',
23});

마치며

Next.js 15의 RSC와 스트리밍은 강력한 도구지만, 올바르게 사용해야 효과가 납니다. 처음부터 완벽한 구조를 만들려 하기보다, 측정 → 병목 확인 → 개선의 루프를 반복하는 것을 권장합니다.

Core Web Vitals 측정은 Chrome DevTools의 Lighthouse, 그리고 실제 사용자 데이터는 Vercel Analytics 또는 Google Search Console Speed Report를 활용하세요.

참고 자료

  • React Server Components Streaming Performance Guide 2026
  • Next.js 15 공식 문서 - Production Checklist
  • Next.js 15 & 16 Features: Migration Guide 2026
  • Next.js 15 공식 릴리스 노트

FAQ

Q. Next.js 15 App Router에서 TTFB를 낮추는 가장 효과적인 방법은? A. 스트리밍 RSC + Suspense 조합입니다. 정적 셸을 즉시 전송하고, 데이터가 필요한 섹션은 Suspense로 감싸 스트리밍으로 채웁니다. 이것만으로 TTFB를 350ms에서 40~90ms로 낮출 수 있습니다.

Q. Next.js에서 "use client"를 어디에 붙여야 하나요? A. useState, useEffect, onClick, onChange 등 브라우저 API나 이벤트 핸들러가 실제로 필요한 컴포넌트에만 붙입니다. 가능한 한 리프(leaf) 컴포넌트에 위치시켜 클라이언트 번들에 포함되는 코드를 최소화합니다.

Q. Next.js 15 Turbopack은 프로덕션 빌드에도 사용 가능한가요? A. 현재(2026년 3월 기준) Turbopack은 개발 서버(next dev)에서 기본 사용됩니다. 프로덕션 빌드(next build)에서의 Turbopack 지원은 진행 중이며, 현재는 Webpack이 프로덕션 빌드에 사용됩니다.