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
가장 흔한 16 마이그레이션 함정입니다. 'use cache'는 "이 스코프 안에서는 동적 데이터(cookies/headers/searchParams)에 절대 접근하지 않겠다"는 약속이라, 안에서 그 함수들을 호출하면 빌드 단계에서 컴파일러가 잡습니다. 15까지는 런타임에 무시되던 게 16에선 빌드 실패로 강제됐어요.
해결 패턴 — 컴포넌트 단위로 분리
"페이지 전체를 캐시"가 아니라 "정적인 부분만 캐시"로 바꿔야 합니다. 다음처럼 분리하세요.
핵심은 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 선언으로 풀어야 합니다.
저도 같은 함정에 빠졌었는데 한 가지 추가하면:
headers()·draftMode()도 동일하게 빌드 에러가 납니다. 특히 generateMetadata 안에서headers()로 호스트네임 받아 OG 이미지 URL 만들던 코드가 16에서 다 깨졌어요.이건 메타데이터 생성 자체가 동적이 되어야 해서, 위 답변처럼 컴포넌트 분리로 해결되지 않습니다. 페이지 옵션으로:
그리고 빌드 에러 메시지에 줄 번호가 안 찍히는 케이스가 있어서 헷갈리는데,
next build --debug-prerender로 돌리면 스택이 더 자세히 나옵니다.route group
(auth)자체는 캐시 동작에 영향이 없는 게 맞습니다. 빌드 결과의 라우트도 동일하게/dashboard가 되고요. 그런데 빌드 통과 여부가 갈리는 건 보통 layout 캐스케이드 때문입니다.예를 들어
app/(auth)/layout.tsx에서cookies()를 호출하고 있으면, 그 안의 모든 페이지가 동적으로 처리되어 'use cache' 위반이 잡힙니다. 같은 코드를app/dashboard로 옮기면 그 layout이 안 거쳐서 통과되는 거예요.한 줄 정리:
app/(auth)/layout.tsx내용을 확인 — cookies/headers/draftMode 호출 여부middleware 패턴 예시:
제 경우 정확히 두 분 다 맞았어요. layout에서 cookies로 인증 체크 + 페이지에 통째 'use cache'. 인증을 middleware로 옮기고 페이지를 부모(동적) + ProjectList(캐시)로 분리하니까 빌드 통과 + LCP도 오히려 더 좋아졌습니다 (480ms → 220ms).
마이그레이션 가이드에서 분명히 봤던 것 같은데 막상 닥치니 헷갈렸네요. 저장해뒀다가 팀 세미나 자료로 쓰겠습니다.