Next.js 16.4 ensureStatic 사용법과 빌드 에러 해결 절차

Next.js 16.4 ensureStatic 사용법은 page나 layout 파일에서 export const ensureStatic = 'navigation'처럼 값을 내보내는 방식이다. ensureStatic은 v16.4.0에서 도입된 App Router route segment config다. 라우트의 App Shell, prefetch, 전체 내비게이션 중 선택한 출력이 요청마다 렌더링되지 않고 정적이도록 요구한다.

이 export는 cacheComponents가 켜진 프로젝트에서만 동작한다. Cache Components는 Node.js 런타임이 필요하다. 따라서 runtime = 'nodejs'를 포함한 모든 runtime export를 먼저 지워야 한다. 이 글은 2026-11 기준 공식 문서와 가이드의 온라인 스토어 예제를 따라 준비, 적용, 에러 해결, 결과 확인 순서로 진행한다.

목차

ensureStatic 값별 의미와 적용 대상

ensurestatic-values-diagram

ensureStatic API 레퍼런스에 정의된 허용값은 'auto', 'shell', 'prefetch', 'navigation', false 다섯 가지다. 기본값 'auto'는 export를 생략한 것과 같다. 타입 정의는 다음과 같다.

type EnsureStatic = 'auto' | 'shell' | 'prefetch' | 'navigation' | false

export const ensureStatic: EnsureStatic = 'navigation'

값마다 정적이어야 하는 출력 범위가 다르다.

값정적이어야 하는 출력
'auto'추가 요구 없음 (export 생략과 동일)
'shell'App Shell
'prefetch'App Shell과 per-link prefetch
'navigation'서버에서 렌더링되는 라우트 전체

false는 정적 레벨을 고르는 값이 아니다. 해당 세그먼트의 일반 렌더링을 유지하고, 다른 세그먼트가 'shell'·'prefetch'·'navigation'을 적용하지 못하게 막는다.

세 레벨이 의존하는 기능도 다르다. 'shell'과 'prefetch'는 Partial Prefetching이 켜져 있어야 의미가 있다. 'navigation'은 Cache Components만으로 동작한다. 레벨을 고르는 기준은 다음과 같다.

  • 레이아웃의 App Shell만 정적으로 보장하려면 'shell'을 쓴다.
  • 링크별 prefetch까지 정적 출력에서 나와야 하면 'prefetch'가 맞다.
  • 페이지 전체에 동적 콘텐츠가 섞이면 빌드를 실패시키려면 'navigation'을 고른다.

Next.js 16.4 릴리스 블로그는 ensureStatic을 “라우트의 shell, prefetch, 전체 내비게이션이 정적임을 보장하는 방법”으로 소개한다. 적용 대상으로는 이커머스 스토어, 마케팅 페이지, 블로그를 든다. root layout에 'navigation'을 두면 사이트 모든 페이지의 내비게이션이 정적임을 보장한다.

릴리스 블로그 예제에서는 'navigation'을 단 페이지 안의 UserAvatar 컴포넌트가 빌드를 실패시키는 대상으로 표시된다.

export const ensureStatic = 'navigation';

export default function Page() {
  return (
    <>
      <UserAvatar /> {/* 🔴 This component fails the build */}
      <Content />
    </>
  );
}

사전 준비: Next.js 16.4 업그레이드와 Cache Components 설정

cache-components-setup-blueprint

1. Next.js를 16.4 이상으로 올리기

릴리스 블로그가 안내하는 업그레이드 명령은 두 가지다. 직접 올릴 때는 첫 번째 명령을, 코딩 에이전트에 맡길 때는 두 번째 명령을 쓴다.

# 직접 업그레이드
npm install next@latest

# 에이전트에 업그레이드 위임
npx next@canary upgrade --agent=latest

create-next-app으로 새로 만드는 앱은 Cache Components가 기본으로 켜져 있다. 기존 앱이라면 다음 단계의 설정을 직접 추가해야 한다.

확인 방법: package.json의 next 버전이 16.4 이상인지 본다.

2. next.config.ts에 cacheComponents와 partialPrefetching 켜기

cacheComponents 설정 문서는 두 옵션을 함께 true로 켜는 설정을 보여 준다.

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
}

export default nextConfig

cacheComponents는 16.0.0에서 experimental.cacheComponents가 top-level 옵션으로 옮겨지며 안정화됐다. 16.4에서는 위처럼 top-level 키로 지정한다. 이 옵션 없이 ensureStatic을 export하면 빌드가 실패한다.

partialPrefetching은 16.3.0에서 도입된 옵션으로, 명시적으로 지정해야 한다. 지정하지 않으면 false처럼 동작하고, cacheComponents가 켜져 있을 때 경고를 남긴다. 값별 의미는 이렇다.

  • true: 앱 전체에 Partial Prefetching을 켠다.
  • false: Partial Prefetching 이전에 Cache Components를 도입한 앱을 위해 기존 prefetch 동작을 유지한다.

반대로 cacheComponents 없이 partialPrefetching만 쓰면 next dev와 next build가 config validation 단계에서 오류를 던진다.

partialPrefetching 옵션 제거 예정
[partialPrefetching 설정 문서](https://nextjs.org/docs/app/api-reference/config/next-config-js/partialPrefetching)에 따르면 다음 메이저 릴리스에서 두 기능이 모든 앱에서 켜지고 옵션 자체는 제거된다. 지금 `true`로 켜 두면 메이저 업그레이드 시점의 동작과 맞춰진다. `false`로 기존 prefetch 동작에 의존하는 앱은 메이저 업그레이드 전에 Partial Prefetching 기준으로 옮겨야 한다.
### 3. runtime export 제거

Cache Components는 Node.js 런타임이 필요하고, runtime route segment config를 지원하지 않는다.

runtime export는 모두 삭제
`runtime = ‘nodejs’`와 deprecated된 `runtime = ‘edge’`를 포함해 page·layout의 `runtime` export를 전부 지워야 한다. Node.js 값이라도 남겨 두면 안 된다. Node.js 외의 서버 측 JavaScript 런타임에서는 동작이 보장되지 않는다.
확인 방법: 프로젝트 전체에서 `export const runtime` 문자열을 검색해 남은 export가 없는지 본다.

4. Server Component에서만 export하기

ensureStatic은 Server Component인 page나 layout에서 export한다. Client Component에서의 export는 에러 발생 원인이다. 'use client'가 붙은 파일에 넣지 않았는지 확인한다.

ensureStatic 단계별 적용: 온라인 스토어 예제

Keeping pages static 가이드는 스토어 레이아웃, 상품 페이지, 가이드 페이지에 세 레벨을 차례로 적용한다. 아래 순서는 그 가이드를 따른다.

Step 1: 스토어 레이아웃에 ‘shell’ 지정

app/store/layout.tsx에는 cookies()로 장바구니 개수를 읽는 CartButton이 Suspense로 감싸져 있다. 이 레이아웃에 'shell'을 추가한다.

import Link from 'next/link'
import { Suspense } from 'react'
import { CartButton } from '@/app/ui/cart'

export const ensureStatic = 'shell'

export default function StoreLayout({ children }: LayoutProps<'/store'>) {
  // ...
}

적용하면 기본 <Link>는 정적 App Shell만 요청한다. 쿠키에 의존하는 장바구니 버튼은 Suspense 경계 안에 있으므로 Shell 밖에 남는다.

'shell'은 per-link prefetch까지 정적으로 강제하지 않는다. 링크별 prefetch도 정적 출력에서 나와야 하는 페이지는 Step 2처럼 'prefetch'를 쓴다.

Step 2: 상품 페이지에 ‘prefetch’ 지정

app/store/products/[slug]/page.tsx에 'prefetch'를 추가한다.

// app/store/products/[slug]/page.tsx
export const ensureStatic = 'prefetch'

<Link prefetch={true}>로 상품 페이지를 prefetch할 때 요청 시점 서버 렌더링 없이 정적 출력만 쓰게 된다.

Step 3: 가이드 페이지에 ‘navigation’ 지정

app/guides/[id]/page.tsx에는 가장 엄격한 'navigation'을 넣는다.

// app/guides/[id]/page.tsx
export const ensureStatic = 'navigation'

값은 export한 세그먼트에만 머물지 않는다. 가이드 원문은 “An ensureStatic value covers the whole route, including layouts above the segment that exports it.”라고 설명한다. 위쪽 layout에 동적 데이터가 있으면 그 layout도 검증 대상이다.

설정을 마쳤으면 빌드를 실행한다.

next build

'navigation'은 next dev와 next build 양쪽에서 검증되므로, 조건을 어기는 코드가 있으면 이 단계에서 빌드가 멈춘다. 예제 프로젝트에서는 두 가지 에러가 차례로 나타난다.

빌드 에러 해결: generateStaticParams 누락과 uncached data

에러 1: generateStaticParams() 누락

'navigation'을 추가한 직후 빌드하면 다음 에러가 먼저 난다.

Error: Page "/guides/[id]": `ensureStatic = "navigation"` requires an exported `generateStaticParams()` function.
Learn more: https://nextjs.org/docs/messages/generate-static-params#with-ensurestatic
    at ignore-listed frames
> Build error occurred
Error: Failed to collect page data for /guides/[id]

동적 라우트에 'navigation'을 달았다면 generateStaticParams()를 export해야 한다. 이 함수는 라우트의 모든 동적 파라미터를 포함한 완전한 파라미터 세트를 1개 이상 반환해야 한다. /guides/[id]라면 id 값을 담은 세트가 최소 하나 필요하다.

에러 2: uncached or runtime data

generateStaticParams를 추가한 뒤에도 캐시되지 않은 DB 쿼리와 cookies()가 남아 있으면 다음 메시지로 빌드가 실패한다.

Next.js encountered uncached or runtime data on a route that must be fully static

에러 메시지는 세 가지 해결 방향을 제시한다.

  • [cache]: 해당 데이터 접근에 'use cache'를 붙인다.
  • [remove]: 요청 의존 데이터 접근 자체를 지운다.
  • [client]: 그 값을 클라이언트에서 읽도록 옮긴다.

가이드의 해결 순서는 getGuide()에 'use cache'를 추가해 DB 쿼리를 캐시한 다음, cookies() 접근을 제거하는 것이다.

원인 코드 위치 찾기

빌드 스택 프레임에는 소스 위치가 표시되지 않는다. 원인 위치는 next dev로 /guides/coffee를 열어 개발 오버레이에서 확인한다.

CI처럼 빌드만 돌릴 수 있는 환경이라면 디버그 옵션을 붙인다. 기본 빌드 출력은 축약돼 있고, 이 옵션을 쓰면 자세한 스택 트레이스와 코드 프레임이 나온다.

next build --debug-prerender
flowchart TD
  A["ensureStatic = 'navigation' 추가"] --> B[next build]
  B --> C{generateStaticParams 있음?}
  C -->|없음| D[generateStaticParams export]
  D --> B
  C -->|있음| E{uncached/runtime data?}
  E -->|있음| F["'use cache' 추가 또는 접근 제거"]
  F --> B
  E -->|없음| G[○ Static 출력]

결과 확인: next build 출력의 ○와 ◐

수정 전 빌드 출력에서 /guides/[id]는 ◐ (Partial Prerender)로만 표시된다. DB 쿼리를 캐시하고 cookie 접근을 제거하면 generateStaticParams가 반환한 가이드가 ○ (Static)으로 바뀐다. 수정 후 출력은 다음과 같다.

Route (app)
├   /guides/[id]
│ ├ ◐ /guides/[id]
│ └ ○ /guides/coffee
├ ◐ /store
└   /store/products/[slug]
  ├ ◐ /store/products/[slug]
  └ ◐ /store/products/coffee-grinder

○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

판단 기준은 'navigation'을 단 경로가 ○ 기호와 “prerendered as static content” 범례로 표시되는지다. 예제에서는 /guides/coffee가 그 상태다.

목록에 없는 가이드 ID는 첫 요청 때 Next.js가 완전한 정적 페이지를 생성해 저장할 때까지 기다린다. 자주 열리는 ID는 generateStaticParams에 미리 넣어 두면 빌드 시점에 생성된다.

적용 이후의 회귀도 같은 검증으로 잡힌다. 가이드 원문은 “ensureStatic will report future changes that would break that requirement.”라고 적는다. 누군가 'navigation' 페이지에 cookies()를 다시 넣으면 다음 빌드에서 실패한다.

shell·prefetch는 빌드로 검증되지 않는다
`’shell’`과 `’prefetch’`는 별도 빌드 검증을 추가하지 않는다. 그래서 빌드가 성공했다는 사실만으로 두 레벨의 적용 여부를 판단할 수 없다. 위 출력에서 `/store`와 상품 경로가 ◐로 남아 있는 것도 이 때문에 빌드 실패로 이어지지 않는다.
## 주의사항: navigation 검증 조건과 레벨 조합 규칙

Suspense로도 피할 수 없는 서버 콘텐츠

'navigation' 검증에서는 다음 서버 콘텐츠가 Suspense로 감싸거나 instant = false를 설정해도 실패한다.

  1. Uncached data: 'use cache' 없는 fetch·DB 호출, 그리고 connection()
  2. Runtime data: cookies(), headers(), 서버 쪽 searchParams
  3. 짧은 수명의 use cache 스코프: expire 5분 미만, stale 30초 미만, revalidate 0 중 하나라도 해당하는 경우 (stale 30초~5분은 허용)
  4. 요청 의존 promise: Client Component에 넘긴, 요청에 의존하는 서버 데이터 promise

이 제약은 페이지 본문에만 걸리지 않는다. generateMetadata와 generateViewport도 같은 규칙으로 검사되므로, 메타데이터 생성 함수 안의 headers() 호출도 빌드를 실패시킨다.

'use cache'를 붙였더라도 수명 설정이 3번 조건에 걸리면 통과하지 못한다. 'navigation' 라우트에서 캐시 수명을 짧게 잡을 때는 stale 30초 이상, expire 5분 이상인지 확인한다.

부모·자식 세그먼트의 레벨 조합

root layout에 지정한 레벨은 앱의 모든 라우트에 적용된다. 이 상태에서 하위 세그먼트가 고를 수 있는 값은 제한된다.

  • export 생략이나 'auto'는 어떤 레벨과도 호환된다.
  • 하위 세그먼트는 부모보다 더 엄격한 레벨만 지정한다. root layout이 'shell'이면 하위 page는 'prefetch'나 'navigation'으로 올릴 수 있다.
  • 더 약한 레벨을 지정하거나, 부모와 자식에서 false를 'shell'·'prefetch'·'navigation'과 섞으면 빌드가 실패한다.
  • 함께 렌더링되는 병렬 라우트 슬롯은 같은 값을 쓰거나, export를 생략하거나, 'auto'를 써야 한다.

규칙을 어겼을 때 나오는 메시지는 다음 세 가지다.

A child segment cannot override a parent segment with a less-constrained `ensureStatic`.

A child segment cannot override a parent segment with an incompatible `ensureStatic`.

Parallel slots cannot have incompatible `ensureStatic`.

layout에 레벨을 둘 때도 형태는 page와 같다. 레퍼런스의 layout 예제는 다음과 같다.

export const ensureStatic = 'navigation'

export default function Layout({ children }: { children: React.ReactNode }) {
  return <>{children}</>
}

Partial Prefetching이 꺼져 있을 때

partialPrefetching이 false이거나 지정되지 않은 상태에서는 결과가 레벨마다 다르다. 'shell'은 App Shell 자체가 없으므로 효과가 없고 Next.js가 경고를 남긴다. 'prefetch'는 아무 요구도 추가하지 않으며, 링크는 기존 full-prefetch 동작을 유지한다. 'navigation'만 그대로 동작한다.

관련 글

자주 묻는 질문

Pages Router에서도 ensureStatic을 쓸 수 있나?

ensureStatic 레퍼런스와 Keeping pages static 가이드는 App Router의 page·layout export만 설명한다. Pages Router에서의 사용법은 두 문서에서 다루지 않는다.

instant = false로 navigation 검증을 끌 수 있나?

끌 수 없다. instant = false는 Instant Navigation 검증을 끄는 설정이고, 'navigation'의 완전 정적 출력 검증은 완화하지 않는다. 두 export는 서로 다른 검증을 담당한다.

특정 페이지만 일반 렌더링으로 남기려면?

해당 세그먼트에 false를 지정하면 일반 렌더링이 유지된다. 단, 부모 layout이 이미 'shell'·'prefetch'·'navigation' 중 하나를 쓰고 있으면 false와 섞인 조합이 되어 빌드가 실패한다. 이 경우 레벨을 부모가 아닌 개별 page 쪽으로 내려서 지정해야 한다.

다음 단계: 캐시 수명과 정적 파라미터 설계

ensureStatic의 'navigation' 검증을 통과하는지는 대부분 'use cache' 스코프의 수명 설정과 generateStaticParams가 반환하는 파라미터 목록에서 갈린다. 이어서 볼 주제는 Next.js use cache의 stale·expire·revalidate 설정 방식, 그리고 동적 라우트에서 generateStaticParams와 ensureStatic을 함께 쓰는 패턴이다. 기존 프로젝트라면 Next.js 16.4 업그레이드 과정에서 Cache Components와 Next.js partialPrefetching을 켜는 마이그레이션이 선행 작업이다.

이 글 공유하기