Next.js 16.4 업그레이드: ensureStatic으로 정적 보장, navigation()으로 프리페치 범위 줄이기
next upgrade --agent부터 ensureStatic 단계 선택, 빌드 실패 해결, navigation()·prefetch() 배치, 실험 플래그 선택까지

핵심 요약
- ensureStatic은 cacheComponents가 켜져 있어야 하며, shell·prefetch는 partialPrefetching까지 켜야 의미가 있다.
- 'navigation'만 next dev·next build에서 전체 정적 출력을 검증하고, 동적 라우트는 generateStaticParams()가 필수다.
- 자식 세그먼트는 부모보다 강한 단계로만 바꿀 수 있으므로 정적 구역의 레이아웃에서 시작해 아래로 좁힌다.
- navigation()은 링크별 프리페치에서도 빼고, prefetch()는 App Shell에서만 빼며, 둘 다 아래 코드의 use cache 수명을 유지한다.
- experimental.agentUpgrade 기본값은 security이고 알림만 띄우며 자동 업그레이드는 하지 않는다.
Next.js 16.x App Router 앱을 운영하면서 Cache Components 도입 시점을 고민하던 개발자를 위한 글이다. 16.4부터 Next.js 팀은 Cache Components를 모든 앱에 권장하고, create-next-app으로 만든 새 프로젝트에는 기본으로 켜 둔다(Next.js 16.4 릴리스 노트). 이 글을 따라가면 기존 앱을 16.4로 올리고, ensureStatic으로 정적이어야 할 라우트를 빌드 단계에서 지키고, navigation()·prefetch()로 프리페치가 서버에 주는 부담을 줄이는 작업까지 할 수 있다.
use cache, cacheTag 같은 캐시 기초는 Next.js 16 캐싱 정리에서 다뤘으므로 여기서는 16.4에서 새로 생긴 설정과 함수에 집중한다.
업그레이드 전에 확인할 버전과 설정
먼저 현재 상태를 기록해 둔다. 업그레이드 후 빌드 결과를 비교할 기준이 된다.
npx next --version
node -v
grep -nE "cacheComponents|partialPrefetching|experimental" next.config.*
npx next build 2>&1 | tail -40 > build-before.txt
확인할 것은 세 가지다. Next.js가 16.x인지(15 이하라면 16 마이그레이션 가이드를 먼저 거쳐야 한다), next.config에 cacheComponents가 이미 켜져 있는지, 빌드 출력에서 각 라우트가 정적(○)인지 동적(ƒ)인지다. 마지막 항목은 나중에 ensureStatic을 어느 레이아웃에 걸지 정할 때 그대로 쓰인다.
두 가지 업그레이드 경로와 고르는 기준
| 방식 | 명령 | 맞는 상황 |
|---|---|---|
| 에이전트 업그레이드 | npx next@canary upgrade --agent=latest | Codex·Claude Code 같은 코딩 에이전트를 쓰고, 마이그레이션 가이드·codemod·검증까지 맡기고 싶을 때 |
| 수동 설치 | npm install next@latest | 패치·마이너 업그레이드만 필요하거나, 변경을 직접 통제해야 할 때 |
에이전트 업그레이드 가이드에 따르면 --agent 옵션은 설치된 버전과 릴리스 정보를 확인해 목표 버전을 고르고, 관련 마이그레이션 가이드와 Skills를 임시 디렉터리에 준비한 뒤 에이전트에게 작업을 넘긴다. 인터랙티브 터미널에서는 설치된 Codex 또는 Claude Code 중 하나를 고르고, 별도 Git worktree에서 진행할지도 선택할 수 있다. 이미 에이전트 세션 안에서 실행하면 작업 지시문만 출력해 현재 에이전트가 따르게 한다. 이 기능은 문서에 실험 기능으로 표시돼 있으므로 결과 커밋은 사람이 검토한다.
정책 값은 세 가지다. security는 취약점이 있는 안정 버전을 같은 메이저 안에서 안전한 릴리스로 올리고, latest는 npm의 latest 안정 버전으로 올린다. experimental-future는 latest를 적용한 뒤 Cache Components 같은 향후 기본값 도입까지 준비한다. 모노레포라면 npx next@canary upgrade apps/web --agent=latest처럼 앱 경로를 넘긴다. --agent는 --revision과 함께 쓸 수 없다.
에이전트에게 맡기기 전에 작업 공간을 분리해 두면 되돌리기가 쉽다. 절차는 Git Worktree로 AI 에이전트 작업 격리하기를 참고한다.
Cache Components와 Partial Prefetching 켜기
16.4 문서는 Cache Components 모델을 두 개의 플래그로 설명한다. ensureStatic의 shell·prefetch 단계와 prefetch() 함수는 Partial Prefetching이 켜져 있어야 의미가 있으므로 둘을 함께 켠다.
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
}
export default nextConfig
기존 앱 전체에 한 번에 켜기 부담스럽다면 공식 문서의 Partial Prefetching 도입 가이드가 라우트 단위 적용 방법을 따로 안내한다. 16.3에서 소개된 Instant Navigations 동작은 Next.js 16.3 Instant Navigations 적용기에 정리해 두었다.
ensureStatic 세 단계 중 어느 것을 걸지 정하기
ensureStatic 레퍼런스에 따르면 이 export는 페이지나 레이아웃에서 내보내며, 정적이어야 하는 출력의 범위를 정한다. 뒤 단계일수록 앞 단계를 포함한다.
| 값 | 정적이어야 하는 출력 | Partial Prefetching 필요 | 빌드 검증 |
|---|---|---|---|
'auto'(기본) | 추가 요구 없음 | - | 없음 |
'shell' | 기본 <Link>가 받는 App Shell | 예(없으면 경고만 출력) | 별도 검증 없음 |
'prefetch' | App Shell + <Link prefetch>의 링크별 프리페치 | 예 | 별도 검증 없음 |
'navigation' | 서버 렌더링 결과 전체 | 아니오(Cache Components만 필요) | next dev·next build에서 검증 |
false | 정적 최적화 대상이 아님을 표시 | - | 상위가 단계를 걸면 빌드 실패 |
블로그·문서·마케팅 페이지처럼 요청마다 달라질 내용이 없는 영역은 'navigation'이 맞다. 로그인 사용자 정보가 일부 들어가는 대시보드는 'shell'이나 'prefetch'로 껍데기만 정적으로 묶고, 사용자별 내용은 <Suspense> 안에서 나중 단계로 넘긴다. cacheComponents가 꺼져 있으면 export 자체가 빌드 실패를 일으키고, Client Component 파일에서 내보내면 오류가 난다.
레이아웃에서 시작해 아래로 좁히는 순서
- 정적이어야 할 구역의 레이아웃에 건다 — 예를 들어
app/(marketing)/layout.tsx에export const ensureStatic = 'navigation'을 넣으면 그 아래 페이지·중첩 레이아웃·병렬 라우트 전체에 적용된다. next dev로 위반을 찾는다 — 개발 서버는 소스맵이 붙은 스택 트레이스를 보여 준다. 빌드 출력은 짧으므로 원인이 안 보이면next build --debug-prerender로 다시 실행한다.- 동적 라우트에
generateStaticParams()를 추가한다 —'navigation'에서는 동적 세그먼트마다 완전한 파라미터 세트를 하나 이상 반환해야 하며, 없으면 빌드가 실패한다. - 하위에서 더 엄격하게만 바꾼다 — 자식 세그먼트는 부모보다 강한 단계로만 바꿀 수 있다. 부모가
'prefetch'일 때 자식 페이지를'navigation'으로 올리는 것은 허용된다.
// app/(marketing)/layout.tsx
export const ensureStatic = 'navigation'
// app/(marketing)/pricing/[plan]/page.tsx
export async function generateStaticParams() {
return [{ plan: 'free' }, { plan: 'team' }]
}
빌드가 실패하는 대표 원인과 고치는 방향
'navigation'에서는 <Suspense>로 감싸도 다음 항목이 검증에 걸린다. use cache 없는 fetch·DB 호출과 connection(), cookies()·headers()·서버 쪽 searchParams, expire가 5분 미만이거나 stale이 30초 미만이거나 revalidate가 0인 캐시, 요청에 의존하는 Promise를 Client Component에 넘기는 경우다. 같은 제약이 generateMetadata와 generateViewport에도 적용된다.
| 터미널 메시지 / 증상 | 원인 | 해결 방향 |
|---|---|---|
| 캐시되지 않은 데이터 접근 오류 | 정적 구역 안의 DB 호출에 use cache가 없음 | 데이터 함수에 use cache와 5분 이상 cacheLife 지정 |
| 런타임 데이터 접근 오류 | 헤더의 로그인 표시가 cookies()를 읽음 | 해당 UI를 Client Component로 옮기거나, 그 구역을 'navigation' 범위 밖 레이아웃으로 분리 |
A child segment cannot override a parent segment with a less-constrained `ensureStatic`. | 자식이 부모보다 약한 단계 지정 | 자식 export 제거 또는 같은/더 강한 단계로 변경 |
Parallel slots cannot have incompatible `ensureStatic`. | 함께 렌더링되는 병렬 슬롯끼리 값이 다름 | 같은 값 또는 미지정으로 통일 |
navigation()과 prefetch()로 미리 받을 범위 줄이기
상품 목록 화면에 상품 상세 링크가 수십 개 보이고, 각 링크에 prefetch를 켰다고 하자. 상세 페이지의 상품 정보는 미리 받아 두면 이동이 빨라지지만, 리뷰 목록까지 링크마다 미리 렌더링하면 사용자가 열지 않을 상품의 리뷰 조회가 서버에서 계속 실행된다. navigation() 레퍼런스는 이런 경우를 위해 await navigation() 아래의 작업을 App Shell과 링크별 프리페치에서 빼고, 사용자가 실제로 이동할 때 실행하도록 한다.
// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { cacheLife, navigation } from 'next/cache'
export default function Page({ params }: PageProps<'/products/[id]'>) {
return (
<>
<Suspense fallback={<p>상품 정보를 불러오는 중</p>}>
<ProductInfo params={params} /> {/* 링크별 프리페치에 포함 */}
</Suspense>
<Suspense fallback={<p>리뷰를 불러오는 중</p>}>
<Reviews params={params} /> {/* fallback만 프리페치에 포함 */}
</Suspense>
</>
)
}
async function Reviews({ params }: { params: Promise<{ id: string }> }) {
await navigation()
const { id } = await params
return <ReviewList items={await getReviews(id)} />
}
async function getReviews(id: string) {
'use cache'
cacheLife('hours')
return db.reviews.list({ productId: id })
}
배치에서 지킬 규칙이 몇 가지 있다. navigation()은 지연시킬 코드보다 위에 두고 그 컴포넌트를 <Suspense>로 감싼다. 형제 컴포넌트에는 영향이 없다. use cache 스코프 안에서 호출하면 오류가 나므로, 위 예처럼 캐시 지시어는 그 아래에서 부르는 함수에 붙인다. 페이지 첫 로드에는 효과가 없고 클라이언트 이동에만 적용된다.
prefetch()는 한 단계 앞을 다룬다. 기본 <Link>가 받는 App Shell에서는 빼되, <Link prefetch>가 있으면 그때 미리 받게 한다. prefetch() 레퍼런스에 따르면 Partial Prefetching이 꺼진 라우트에서는 App Shell이 없으므로 효과가 없다. 같은 파일에서 prefetch 세그먼트 설정을 함께 쓰면 이름이 겹치므로 import { prefetch as prefetchStage } from 'next/cache'처럼 별칭을 붙인다.
| 함수 | App Shell | 링크별 프리페치 | 실제 이동 | 요청 의존으로 표시 |
|---|---|---|---|---|
prefetch() | 제외 | 포함 | 포함 | 아니오 |
navigation() | 제외 | 제외 | 포함 | 아니오 |
connection() | 제외 | 제외 | 요청마다 실행 | 예 |
세 함수의 차이는 캐시 유지 여부에 있다. connection() 아래 코드는 요청 의존 작업이 되어 캐시를 쓸 수 없지만, navigation()·prefetch() 아래 코드는 use cache로 수명을 유지할 수 있다.
업그레이드 알림 정책 agentUpgrade 설정
16.4는 next dev·next build 실행 중 관련 업그레이드가 있으면 알려 주는 experimental.agentUpgrade를 추가했다. 문서상 기본 정책은 'security'이고, 자동으로 업그레이드하지는 않는다. 알림에서 Upgrade now를 고르면 명령을 멈추고 에이전트 업그레이드를 시작하며, Skip until next version은 현재 설치 버전에 대한 알림을 끈다.
// next.config.ts
const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
experimental: {
agentUpgrade: 'security', // 'latest' | false
},
}
CI 로그에 알림이 섞이는 것이 싫다면 false로 끄고, 보안 패치는 Dependabot 같은 별도 경로로 받는다. 개발자가 에이전트 도구를 일상적으로 쓰는 팀이라면 기본값을 유지하는 쪽이 취약 버전에 머무르는 기간을 줄인다. 함께 소개된 experimental.agentFeedback은 Next.js 텔레메트리가 켜져 있어야 동작하고 CI에서는 실행되지 않는다.
함께 켤 만한 실험 플래그 고르기
다음 플래그는 모두 experimental 아래에 있어 이후 버전에서 이름이나 동작이 바뀔 수 있다. 문제가 생겼을 때 원인을 좁히려면 한 번에 하나씩 켜고 개발 서버 메모리·빌드 시간을 비교한다.
| 플래그 | 하는 일 | 켤 만한 경우 |
|---|---|---|
turbopackGc: true | 쓰지 않는 컴파일 결과를 메모리·디스크 캐시에서 정리 | 개발 서버를 오래 띄워 두는 대형 앱 |
turbopackLazyDynamicImports: true | 클라이언트 import() 대상을 브라우저가 요청할 때 컴파일 | 버튼 클릭 후 불러오는 무거운 라이브러리가 많은 앱(일부 next/dynamic은 여전히 즉시 컴파일) |
turbopackPluginRuntimeStrategy: 'workerThreads' | Babel·PostCSS·webpack 로더를 별도 프로세스 대신 워커 스레드에서 실행 | 로더가 많은 프로젝트. 단, Node.js 24.13.1 이상에서는 Node 버그로 자식 프로세스 방식으로 되돌아감 |
turbopackRustReactCompiler: true | Babel 대신 Rust 버전 React Compiler 사용(reactCompiler: true와 함께) | 이미 React Compiler를 쓰는 앱 |
turbopackAdditionalRoots | 프로젝트 루트 밖의 심링크 의존성 경로 추가 | 모노레포 로컬 패키지, pnpm 전역 가상 스토어 |
16.4에는 React 19.3이 포함되며, 터보팩 디스크 캐시 압축 방식 변경과 프로덕션 CSS Module 클래스명 단축처럼 설정 없이 적용되는 개선도 있다. 업그레이드 뒤에는 처음에 저장한 build-before.txt와 새 빌드 출력을 비교해 라우트별 정적/동적 표시가 의도와 다르게 바뀌지 않았는지 확인한다. 번들 크기 변화가 Core Web Vitals에 미치는 영향은 Next.js Core Web Vitals 개선의 점검 순서를 따라 확인할 수 있다.
초보자가 자주 실수하는 포인트
- cacheComponents 없이 ensureStatic만 내보내 빌드가 실패하는 경우
- Partial Prefetching을 켜지 않은 채 'shell'을 걸고 정적 보장이 됐다고 생각하는 경우(경고만 출력됨)
- use cache 함수 안에서 await navigation()을 호출해 오류가 나는 경우
- cacheLife를 짧게 잡아 'navigation' 검증에서 실패하는 경우(expire 5분 미만, stale 30초 미만)
체크리스트
- 업그레이드 전 빌드 출력(라우트별 정적/동적)을 파일로 저장했는가
- next.config에 cacheComponents와 partialPrefetching이 켜져 있는가
- 'navigation'을 건 구역의 동적 라우트마다 generateStaticParams()가 있는가
- navigation()·prefetch()를 호출하는 컴포넌트가 Suspense로 감싸져 있는가
- 실험 플래그를 하나씩 켜고 개발 서버 메모리·빌드 시간을 비교했는가
자주 묻는 질문
instant 세그먼트 설정과 ensureStatic은 무엇이 다른가요?
instant는 이동 직후 화면이 즉시 바뀔 수 있는지를 검증하며, 캐시된 UI가 먼저 보이고 요청별 내용은 Suspense 뒤에서 나중에 와도 통과합니다. ensureStatic은 선택한 단계의 서버 출력에 요청 시점 작업이 없어야 한다는 요구입니다. instant = false로 꺼도 ensureStatic = 'navigation'의 검증은 완화되지 않습니다.
Pages Router 프로젝트도 next upgrade --agent를 쓸 수 있나요?
공식 문서에 따르면 App Router와 Pages Router 모두 지원합니다. 다만 experimental-future 정책의 Cache Components 도입 준비는 App Router 프로젝트에만 적용됩니다.
참고 자료 · 검증 기준
- Next.js Blog — Next.js 16.4
- Next.js Docs — ensureStatic
- Next.js Docs — navigation
- Next.js Docs — prefetch
- Next.js Docs — Upgrade with a coding agent
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.