Skip to content

필사 모드: Next.js 16 아키텍처 — Turbopack과 Cache Components 깊이 보기

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

Next.js는 단순한 React 프레임워크를 넘어, 렌더링 전략과 캐시 계층, 그리고 빌드 도구까지 하나로 묶은 풀스택 플랫폼으로 진화했습니다. Next.js 16에 이르러 두 가지 큰 변화가 자리를 잡았습니다. 하나는 Rust로 작성된 번들러 **Turbopack이 기본값이자 안정 단계로 들어선 것**이고, 다른 하나는 부분 사전 렌더링(PPR)과 `use cache`를 묶은 **Cache Components 모델**입니다.

이 글은 "어떤 API를 호출하는가"보다 "프레임워크 안에서 요청이 어떻게 흐르고, 무엇이 어디서 렌더링되며, 결과가 어느 계층에 캐시되는가"를 그림으로 풀어내는 데 집중합니다. 버전마다 세부 동작이 달라질 수 있으므로, 정확한 플래그와 기본값은 항상 공식 문서(nextjs.org)를 함께 확인하시길 권합니다.

1. App Router 전체 구조

App Router는 파일 시스템 기반 라우팅을 한 단계 더 밀어붙여, 디렉터리 자체가 라우트 세그먼트가 되고 특수 파일이 각 세그먼트의 역할을 정의합니다.

app/

├── layout.tsx ← 루트 레이아웃 (모든 페이지 공통, 서버 컴포넌트)

├── page.tsx ← "/" 라우트 진입점

├── loading.tsx ← Suspense 경계의 폴백 UI

├── error.tsx ← 에러 경계 (클라이언트 컴포넌트)

├── not-found.tsx ← 404 처리

├── (marketing)/ ← 라우트 그룹 (URL에 영향 없음)

│ └── about/

│ └── page.tsx ← "/about"

└── dashboard/

├── layout.tsx ← 중첩 레이아웃 (dashboard 하위 공통)

├── page.tsx ← "/dashboard"

└── [id]/

└── page.tsx ← "/dashboard/:id" 동적 세그먼트

각 세그먼트의 특수 파일은 렌더링 트리에서 정해진 위치로 합성됩니다. 레이아웃은 중첩되며 상태를 보존하고, 그 사이에 Suspense와 에러 경계가 자동으로 끼어듭니다.

┌──────────────────────────────────────────────┐

│ RootLayout │

│ ┌──────────────────────────────────────────┐ │

│ │ DashboardLayout │ │

│ │ ┌────────────────┐ ┌──────────────────┐ │ │

│ │ │ <ErrorBoundary>│ │ <Suspense> │ │ │

│ │ │ error.tsx │ │ loading.tsx │ │ │

│ │ │ ▼ │ │ ▼ │ │ │

│ │ │ page.tsx ────────────▶ 렌더 결과 │ │ │

│ │ └────────────────┘ └──────────────────┘ │ │

│ └──────────────────────────────────────────┘ │

└──────────────────────────────────────────────┘

레이아웃은 **자식이 바뀌어도 다시 마운트되지 않습니다**. 이 점이 페이지 전환 시 사이드바나 헤더 상태를 유지하는 핵심입니다.

2. 서버 컴포넌트와 클라이언트 컴포넌트의 경계

App Router의 가장 근본적인 개념은 **React Server Components(RSC)**입니다. 기본적으로 모든 컴포넌트는 서버 컴포넌트이며, 상호작용이 필요한 지점에서만 `"use client"`로 경계를 긋습니다.

서버 컴포넌트 (기본) 클라이언트 컴포넌트 ("use client")

──────────────────────── ──────────────────────────────

DB/파일/시크릿 직접 접근 O 브라우저 API/이벤트 핸들러 O

async/await 데이터 패칭 O useState/useEffect 훅 O

번들에 JS 포함 X 번들에 JS 포함 O

이벤트 핸들러 X 서버 시크릿 접근 X

경계의 핵심 규칙은 "한 번 클라이언트면 그 아래는 모두 클라이언트"라는 점입니다. 다만 서버 컴포넌트를 클라이언트 컴포넌트의 **children/props로 끼워 넣는** 패턴으로 이 경계를 우회할 수 있습니다.

// server-page.tsx (서버 컴포넌트)

export default async function Page() {

const data = await fetchData() // 서버에서만 실행

return (

{/* ServerWidget은 서버에서 렌더되어 RSC 페이로드로 전달됨 */}

)

}

// client-shell.tsx

'use client'

export default function ClientShell({ children }: { children: React.ReactNode }) {

const [open, setOpen] = useState(false)

// children(=ServerWidget)은 이미 직렬화된 RSC 트리, 다시 렌더 안 함

return <div onClick={() => setOpen((v) => !v)}>{open && children}</div>

}

이 합성 패턴은 "상호작용 껍데기는 클라이언트, 무거운 데이터 렌더링은 서버"라는 이상적인 분리를 가능하게 합니다.

직렬화 경계

서버에서 클라이언트로 props를 넘길 때는 직렬화 가능한 값만 전달됩니다. 함수, 클래스 인스턴스, Date 외 복잡한 객체는 경계를 넘지 못합니다. 서버 액션(`"use server"`)만이 함수를 경계 너머로 참조 가능하게 만드는 예외입니다.

[서버 컴포넌트] ──직렬화 가능한 props──▶ [클라이언트 컴포넌트]

│ ▲

└──── "use server" 액션 참조 ────────────┘

(함수 자체가 아닌 참조 ID 전달)

3. 렌더링 모델: SSR / SSG / ISR / PPR

Next.js는 하나의 코드베이스 안에서 여러 렌더링 전략을 섞어 쓸 수 있습니다. 핵심은 "이 콘텐츠가 정적인가, 동적인가, 그리고 언제 다시 만들어지는가"입니다.

┌─────────┬───────────────────┬──────────────────┬────────────────┐

│ 전략 │ 생성 시점 │ 캐시 │ 적합한 경우 │

├─────────┼───────────────────┼──────────────────┼────────────────┤

│ SSG │ 빌드 타임 │ CDN 영구 │ 블로그/문서 │

│ ISR │ 빌드 + 주기 재생성 │ CDN + revalidate │ 상품/뉴스 │

│ SSR │ 매 요청 │ 캐시 없음(기본) │ 개인화/실시간 │

│ PPR │ 정적 셸 + 동적 구멍│ 셸은 CDN, 구멍은 │ 혼합 페이지 │

│ │ │ 요청 시 스트림 │ │

└─────────┴───────────────────┴──────────────────┴────────────────┘

PPR(Partial Prerendering)의 직관

전통적으로 한 페이지는 "전부 정적" 또는 "전부 동적" 중 하나여야 했습니다. PPR은 이 이분법을 깹니다. 정적인 **셸(shell)**을 빌드 타임에 만들어 즉시 내려주고, 동적인 부분만 Suspense 경계로 표시해 요청 시점에 스트리밍으로 채웁니다.

요청 ──▶ [정적 셸 즉시 전송 (CDN 캐시)]

┌────────────────────────────────┐

│ 헤더 / 네비 / 레이아웃 (정적) │ ◀── TTFB 매우 빠름

│ ┌──────────────────────────┐ │

│ │ <Suspense> │ │

│ │ 동적 영역 (요청 시) │ │ ◀── 스트리밍으로

│ │ 예: 사용자 카트, 추천 │ │ 이어서 도착

│ └──────────────────────────┘ │

│ 푸터 (정적) │

└────────────────────────────────┘

사용자는 정적 셸을 거의 즉시 보고, 동적 구멍은 준비되는 대로 채워집니다. 정적/동적의 장점을 한 페이지에서 동시에 누리는 셈입니다.

4. Cache Components — PPR과 use cache의 결합

Next.js 16의 캐시 모델은 명시성을 강조하는 방향으로 정리되었습니다. 과거의 암묵적 캐싱이 혼란을 부르자, **무엇을 캐시할지 개발자가 명시적으로 선언**하는 방식으로 무게중심이 옮겨졌습니다. 그 중심에 `use cache` 지시문이 있습니다.

// 함수 단위 캐싱

async function getProducts(category: string) {

'use cache'

const res = await fetch(`https://api.example.com/products/${category}`)

return res.json()

}

// 컴포넌트 단위 캐싱 + 재검증 태그

async function ProductList({ category }: { category: string }) {

'use cache'

cacheLife('hours') // 캐시 수명 프로파일

cacheTag(`cat-${category}`) // 태그 기반 무효화

const products = await getProducts(category)

return <ul>{/* ... */}</ul>

}

`use cache`로 감싼 영역은 캐시 가능한 정적 영역으로 취급되고, 감싸지 않은 채 동적 데이터(쿠키, 헤더, `searchParams` 등)에 접근하는 영역은 자연스럽게 PPR의 "동적 구멍"이 됩니다. 즉 **Cache Components는 PPR의 정적/동적 경계를 `use cache` 선언으로 결정하는 모델**입니다.

Cache Components 결정 흐름

┌───────────────────────────────────────────────────┐

│ 컴포넌트/함수에 'use cache' 가 있는가? │

└───────────────┬───────────────────┬───────────────┘

예 (있음) 아니오 (없음)

▼ ▼

┌────────────────┐ ┌────────────────────┐

│ 정적 셸에 포함 │ │ 동적 데이터 접근? │

│ CDN/빌드 캐시 │ │ (cookies/headers) │

└────────────────┘ └─────┬──────────┬───┘

예 ▼ 아니오 ▼

┌──────────────┐ ┌──────────────┐

│ 동적 구멍 │ │ 정적 처리 가능 │

│ 요청 시 스트림 │ │ │

└──────────────┘ └──────────────┘

캐시 수명과 태그 무효화

`cacheLife`는 "얼마나 오래", `cacheTag` + `revalidateTag`는 "언제 깨뜨릴지"를 담당합니다.

export async function updateProduct(category: string) {

'use server'

await db.update(/* ... */)

revalidateTag(`cat-${category}`) // 해당 태그가 붙은 캐시만 무효화

}

5. 요청 생명주기 다이어그램

하나의 요청이 들어와 응답으로 나가기까지의 전체 흐름을 정리하면 다음과 같습니다.

브라우저

│ GET /dashboard/42

┌─────────────────────────────────────────────────────────┐

│ Edge / CDN 계층 │

│ 정적 셸 캐시 적중? ──예──▶ 즉시 셸 반환 (스트림 시작) │

│ │ 아니오 │

└─────────┼───────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐

│ Next.js 서버 런타임 │

│ 1) 라우트 매칭 (app/dashboard/[id]/page.tsx) │

│ 2) middleware 실행 (인증/리다이렉트) │

│ 3) 레이아웃 → 페이지 RSC 렌더 시작 │

│ ├─ 'use cache' 영역 → 데이터 캐시 조회 │

│ └─ 동적 영역 → 데이터 패칭(fetch/DB) │

│ 4) RSC 페이로드 생성 + HTML 스트리밍 │

└─────────┬───────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐

│ 데이터 / 캐시 계층 │

│ Data Cache (fetch 결과) · Full Route Cache (RSC) │

│ 외부 DB / API / 파일 시스템 │

└─────────────────────────────────────────────────────────┘

브라우저: HTML 스트림 수신 → 하이드레이션 → 상호작용 가능

6. 캐시 계층 정리

Next.js에는 성격이 다른 여러 캐시가 층층이 쌓여 있습니다. 어느 계층이 어떤 단위를 캐시하는지 구분하는 것이 디버깅의 출발점입니다.

┌────────────────────────────────────────────────────────────┐

│ 1. Router Cache (클라이언트 메모리) │

│ - 방문한 라우트의 RSC 페이로드를 브라우저에 보관 │

│ - 뒤로가기/prefetch 시 즉시 렌더 │

├────────────────────────────────────────────────────────────┤

│ 2. Full Route Cache (서버 빌드/런타임) │

│ - 정적 라우트의 RSC + HTML 결과 │

├────────────────────────────────────────────────────────────┤

│ 3. Data Cache (서버, 영속) │

│ - fetch 결과 / 'use cache' 함수 결과 │

│ - cacheLife / revalidateTag 로 제어 │

├────────────────────────────────────────────────────────────┤

│ 4. Request Memoization (단일 요청 수명) │

│ - 같은 요청 내 동일 fetch 중복 제거 │

└────────────────────────────────────────────────────────────┘

| 캐시 | 위치 | 수명 | 무효화 방법 |

| --- | --- | --- | --- |

| Router Cache | 클라이언트 | 세션/시간 | router.refresh, 내비게이션 |

| Full Route Cache | 서버 | 배포까지 | 재배포, revalidate |

| Data Cache | 서버 | 영속 | revalidateTag, revalidatePath |

| Request Memo | 서버 | 요청 1회 | 자동 |

7. Turbopack — Rust 기반 기본 번들러

Next.js 16에서 Turbopack은 개발 서버와 빌드의 기본 번들러로 자리 잡았습니다. webpack 시대 대비 개발 서버 시작이 약 4배, 갱신(렌더) 반영이 약 50% 빨라졌다고 공식적으로 안내되어 왔습니다. 정확한 수치는 프로젝트 규모와 버전에 따라 달라질 수 있습니다.

webpack (기존) Turbopack (Rust)

────────────────────────── ──────────────────────────

JS 기반 단일 스레드 위주 Rust 멀티코어 병렬 처리

전체 그래프 재계산 경향 함수 단위 증분 캐싱

큰 앱일수록 시작 느림 요청한 라우트만 온디맨드 컴파일

HMR 점진적 변경분만 최소 재계산

Turbopack의 핵심은 **요청 기반 증분 컴파일**과 **세밀한 캐싱**입니다. 전체 앱을 미리 다 빌드하지 않고, 실제로 접근한 라우트와 모듈만 컴파일하며, 변경이 생기면 영향받은 부분만 다시 계산합니다.

파일 변경 (button.tsx)

┌──────────────────────────────┐

│ 의존성 그래프에서 영향 추적 │

│ button.tsx → toolbar.tsx → │

│ page.tsx │

└──────────────┬───────────────┘

영향받은 노드만 무효화 후 재계산

(그래프의 나머지는 캐시 그대로)

HMR 패치를 브라우저로 전송

8. 마이그레이션과 함정

App Router와 Cache Components로 옮겨갈 때 자주 부딪히는 지점을 정리합니다.

8.1 async 데이터 API

요청별 데이터 접근 API(`cookies`, `headers`, `params`, `searchParams` 등)가 비동기로 다뤄지는 방향으로 정리되었습니다. 동기로 접근하던 코드는 `await`를 붙여야 합니다.

// 변경된 패턴

export default async function Page({ params }: { params: Promise<{ id: string }> }) {

const { id } = await params

return <div>{id}</div>

}

8.2 명시적 캐싱으로의 전환

과거에는 `fetch`가 기본적으로 캐시되는 등 암묵적 동작이 있었습니다. 새 모델에서는 캐시를 원하면 `use cache`로 명시하는 사고방식으로 바꾸는 것이 안전합니다. "캐시되겠지"라는 가정 대신 "캐시한다고 선언했는가"를 확인하세요.

8.3 흔한 함정 체크리스트

[ ] 클라이언트 컴포넌트에 서버 전용 모듈(fs, db)을 import 하지 않았는가

[ ] 서버→클라이언트 props가 모두 직렬화 가능한가 (함수/클래스 제외)

[ ] 'use client' 경계를 너무 위쪽에 둬서 번들이 비대해지지 않았는가

[ ] 동적 데이터(cookies/headers) 접근 영역을 Suspense로 감쌌는가

[ ] revalidateTag 의 태그 문자열이 cacheTag 와 정확히 일치하는가

[ ] 환경 변수 중 NEXT_PUBLIC_ 접두사 여부를 혼동하지 않았는가

8.4 경계 위치가 성능을 좌우한다

`"use client"`를 트리 상단(레이아웃)에 두면 그 아래 모든 것이 클라이언트 번들로 끌려 들어갑니다. 상호작용이 필요한 **잎 노드에 가깝게** 경계를 내려두는 것이 번들 크기와 초기 로딩에 결정적입니다.

나쁜 예 좋은 예

───────────── ─────────────

RootLayout ("use client") RootLayout (server)

└─ 전부 클라이언트 번들 └─ Page (server)

└─ LikeButton ("use client")

← 상호작용 잎만 클라이언트

9. 실무 적용 시나리오

전형적인 대시보드 페이지를 Cache Components 관점에서 설계해 봅니다.

/dashboard/[id]

├─ Layout (정적, 'use cache') ← 네비/사이드바

├─ ProfileHeader (동적: cookies 사용) ← 사용자별, Suspense 구멍

├─ StatsPanel ('use cache', cacheLife) ← 5분 캐시, 공용 통계

└─ ActivityFeed (동적: 실시간) ← 스트리밍, Suspense 구멍

이렇게 하면 네비와 공용 통계는 CDN/캐시에서 즉시 내려오고, 사용자별·실시간 영역만 요청 시 채워집니다. 페이지 하나가 정적의 속도와 동적의 신선함을 동시에 갖는 구조입니다.

마치며

Next.js 16의 아키텍처는 "명시성"과 "증분성"이라는 두 키워드로 요약됩니다. Cache Components는 캐시를 암묵에서 명시로 끌어올려 예측 가능하게 만들었고, Turbopack은 빌드를 전체 재계산에서 증분 계산으로 바꿔 개발 경험을 끌어올렸습니다. PPR은 정적과 동적의 오랜 이분법을 한 페이지 안에서 녹여냈습니다.

핵심은 도구가 아니라 사고방식입니다. "이 영역은 정적인가 동적인가", "캐시한다고 선언했는가", "경계를 잎에 가깝게 두었는가"를 습관처럼 묻는다면, 프레임워크의 추상화가 오히려 명료한 설계 언어가 되어 줄 것입니다. 세부 동작과 기본값은 버전에 따라 달라질 수 있으니, 실제 적용 시에는 공식 문서를 함께 확인하시길 권합니다.

참고 자료

- [Next.js 공식 문서 — App Router](https://nextjs.org/docs/app)

- [Next.js — Caching](https://nextjs.org/docs/app/building-your-application/caching)

- [Next.js — Partial Prerendering](https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering)

- [Next.js — Turbopack](https://nextjs.org/docs/app/api-reference/turbopack)

- [Next.js — Server and Client Components](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns)

- [React 공식 문서 — Server Components](https://react.dev/reference/rsc/server-components)

- [Next.js Blog](https://nextjs.org/blog)

- [Vercel — Rendering 개요](https://vercel.com/docs/frameworks/nextjs)

현재 단락 (1/227)

Next.js는 단순한 React 프레임워크를 넘어, 렌더링 전략과 캐시 계층, 그리고 빌드 도구까지 하나로 묶은 풀스택 플랫폼으로 진화했습니다. Next.js 16에 이르러 두 ...

작성 글자: 0원문 글자: 8,918작성 단락: 0/227