본문 바로가기
2026년 5월 2일2분 읽기

Next.js 16에서 'use cache' 안에 cookies() 호출하면 빌드 에러가 나는데 어떻게 해결하나요

김
김영삼
조회 629댓글 5

15에서 16으로 업그레이드한 뒤 빌드가 안 됩니다. 사용자별 헤더가 들어가는 영역까지 통째로 'use cache' 안에 넣어두고 있었는데 16에선 에러로 떠요.

환경: Next.js 16.2.1, React 19.2, App Router, Vercel 배포

에러 메시지:

Error: Route /dashboard used "cookies" inside "use cache".
Accessing dynamic data sources inside a cache scope is not supported.
Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache

문제 코드 (대략 이런 모양입니다):

// app/dashboard/page.tsx
'use cache'

export default async function Dashboard() {
  const user = await getUser()         // 내부에서 cookies() 호출
  const data = await fetchProjects(user.id)
  return <DashboardView user={user} data={data} />
}

구조상 이 페이지의 80%는 캐시되어도 되고, 20%(사용자 헤더·아바타)만 동적이면 됩니다. 'use cache' 자체를 빼면 너무 느려져서 부분 캐싱을 하고 싶은데, 어떤 패턴이 맞나요? force-dynamic으로 통째 빼는 것 말고 다른 방법이 있을까요?

댓글 5

김김영삼6개월 전

가장 흔한 16 마이그레이션 함정입니다. 'use cache'는 "이 스코프 안에서는 동적 데이터(cookies/headers/searchParams)에 절대 접근하지 않겠다"는 약속이라, 안에서 그 함수들을 호출하면 빌드 단계에서 컴파일러가 잡습니다. 15까지는 런타임에 무시되던 게 16에선 빌드 실패로 강제됐어요.


해결 패턴 — 컴포넌트 단위로 분리


"페이지 전체를 캐시"가 아니라 "정적인 부분만 캐시"로 바꿔야 합니다. 다음처럼 분리하세요.


// app/dashboard/page.tsx  (캐시 X, 동적)
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import UserHeader from './UserHeader'
import ProjectList from './ProjectList'

export default async function Dashboard() {
const c = await cookies()
const userId = c.get('uid')?.value
return (
<>
<UserHeader userId={userId} />
<Suspense fallback={<Skel />}>
<ProjectList userId={userId} />
</Suspense>
</>
)
}

// app/dashboard/ProjectList.tsx (캐시 O)
'use cache'
import { cacheLife, cacheTag } from 'next/cache'

export default async function ProjectList({ userId }: { userId: string }) {
cacheLife('minutes')
cacheTag(`projects-${userId}`)
const data = await db.project.findMany({ where: { userId } })
return <ul>{data.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}

핵심은 cookies()는 페이지(부모)에서만, 캐시되는 자식엔 props로 내려준다는 점입니다. 자식은 props만 보고 결과가 결정되니 캐시할 수 있어요.


cacheTag로 사용자별 무효화


각 사용자별 캐시 키를 projects-${userId}로 잡아두면, 프로젝트 추가/삭제 시 revalidateTag(`projects-${userId}`)로 그 사용자 캐시만 비웁니다. 다른 사용자 캐시는 그대로 살아 있어 hit rate가 유지됩니다.


force-dynamic을 쓰면 안 되나?


쓸 수는 있는데 페이지 전체가 매 요청 SSR이 됩니다. P50이 100ms대에서 300~500ms로 늘어나는 게 보통이에요. 데이터 fetch가 무거우면 더 느려집니다. "동적 부분과 정적 부분의 비율"이 기준입니다 — 80:20처럼 정적이 크면 분리, 50:50이면 차라리 통째 동적으로.


참고로 generateMetadata 안에서 cookies()를 부르는 케이스도 같은 에러를 냅니다. 그건 페이지 자체의 dynamic 선언으로 풀어야 합니다.

풀풀스택지훈6개월 전

저도 같은 함정에 빠졌었는데 한 가지 추가하면:


headers()·draftMode()도 동일하게 빌드 에러가 납니다. 특히 generateMetadata 안에서 headers()로 호스트네임 받아 OG 이미지 URL 만들던 코드가 16에서 다 깨졌어요.


이건 메타데이터 생성 자체가 동적이 되어야 해서, 위 답변처럼 컴포넌트 분리로 해결되지 않습니다. 페이지 옵션으로:


// app/post/[slug]/page.tsx
export const dynamic = 'force-dynamic'
// 또는
export async function generateMetadata({ params }) {
const c = await cookies() // ← 이 함수에서 호출하면 페이지 자체가 동적이 됨
// ...
}

그리고 빌드 에러 메시지에 줄 번호가 안 찍히는 케이스가 있어서 헷갈리는데, next build --debug-prerender로 돌리면 스택이 더 자세히 나옵니다.

JJS주영(guest)6개월 전
저도 비슷한 케이스인데 약간 달라서 여쭤봅니다. 저는 app/(auth)/dashboard 같은 route group 안에서만 에러가 나고, 같은 코드를 app/dashboard로 옮기면 빌드 통과합니다. route group이 영향을 주나요?
김김영삼6개월 전

route group (auth) 자체는 캐시 동작에 영향이 없는 게 맞습니다. 빌드 결과의 라우트도 동일하게 /dashboard가 되고요. 그런데 빌드 통과 여부가 갈리는 건 보통 layout 캐스케이드 때문입니다.


예를 들어 app/(auth)/layout.tsx에서 cookies()를 호출하고 있으면, 그 안의 모든 페이지가 동적으로 처리되어 'use cache' 위반이 잡힙니다. 같은 코드를 app/dashboard로 옮기면 그 layout이 안 거쳐서 통과되는 거예요.


한 줄 정리:



  • app/(auth)/layout.tsx 내용을 확인 — cookies/headers/draftMode 호출 여부

  • 인증 체크를 layout이 아니라 middleware로 옮기면 깔끔해집니다


middleware 패턴 예시:


// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(req) {
const token = req.cookies.get('auth')?.value
if (!token) return NextResponse.redirect(new URL('/login', req.url))
return NextResponse.next()
}
export const config = { matcher: ['/dashboard/:path*'] }
야야밤개발(guest)6개월 전
OP입니다. 두 분 답변대로 해서 해결했습니다 🙏
제 경우 정확히 두 분 다 맞았어요. layout에서 cookies로 인증 체크 + 페이지에 통째 'use cache'. 인증을 middleware로 옮기고 페이지를 부모(동적) + ProjectList(캐시)로 분리하니까 빌드 통과 + LCP도 오히려 더 좋아졌습니다 (480ms → 220ms).
마이그레이션 가이드에서 분명히 봤던 것 같은데 막상 닥치니 헷갈렸네요. 저장해뒀다가 팀 세미나 자료로 쓰겠습니다.
Ctrl+Enter로 등록