Next.js 16 캐싱 정리: use cache, cacheTag, updateTag, revalidateTag 적용 순서

Cache Components 활성화부터 Suspense 배치, 변경 후 갱신 함수 선택까지

핵심 요약

  1. cacheComponents를 켜면 캐시는 'use cache'로 명시하고 요청별 부분은 Suspense로 스트리밍한다.
  2. use cache에는 cacheLife를 함께 지정하고, 무효화가 필요하면 cacheTag를 붙인다.
  3. 액션에서 즉시 반영은 updateTag, 지연 허용 콘텐츠는 revalidateTag(tag, 'max')를 쓴다.
  4. revalidateTag의 단일 인자 형태는 deprecated이므로 두 번째 인자를 명시한다.

Next.js 16의 캐싱은 이전 버전과 사고방식이 다르다. cacheComponents를 켜면 "무엇을 캐시할지"를 'use cache'로 명시하고, 요청마다 달라지는 부분은 <Suspense>로 감싸 스트리밍한다. 이 구조에서 정적인 껍데기(static shell)는 미리 렌더링되고 나머지는 요청 시점에 채워진다. 이 글은 Caching 문서(16.3 기준)를 따라 적용 순서를 정리한다. 이전 모델을 쓰는 프로젝트라면 문서의 "Caching and Revalidating (Previous Model)" 가이드를 따로 봐야 한다. 내비게이션 관점의 변화는 Instant Navigations 글에서 다뤘다.

1단계: Cache Components 켜기

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

이 옵션을 켜면 GET Route Handler도 페이지와 같은 프리렌더링 모델을 따른다. 켠 뒤 개발 서버를 실행하면, 프리렌더링 중 끝나지 않는 컴포넌트를 개발 오버레이가 "blocking-route" 같은 안내로 알려 준다. 이 안내가 다음 단계의 작업 목록이 된다.

2단계: 캐시할 데이터와 UI에 'use cache' 붙이기

'use cache'는 비동기 함수나 컴포넌트의 반환값을 캐시한다. 데이터 함수 단위(데이터 수준)와 컴포넌트·페이지 단위(UI 수준) 모두 가능하며, 문서는 모든 캐시 지시어에 cacheLife를 함께 지정하라고 권한다. 함수 인자와 바깥 범위에서 캡처한 값은 자동으로 캐시 키가 된다.

// app/lib/data.ts
import { cacheLife, cacheTag } from 'next/cache'

export async function getProducts(category: string) {
  'use cache'
  cacheLife('hours')
  cacheTag('products')                 // 나중에 이 태그로 무효화
  return db.product.findMany({ where: { category } })
}

3단계: 요청마다 다른 부분은 Suspense로 감싸기

cookies, headers, searchParams, 동적 params 같은 런타임 API를 읽거나, 매 요청마다 새 데이터가 필요한 컴포넌트는 캐시하지 말고 <Suspense>로 감싼다. 그러면 대체 UI가 정적 껍데기에 포함되고 실제 내용은 요청 시점에 스트리밍된다.

import { Suspense } from 'react'
import { cookies } from 'next/headers'

export default function Page() {
  return (
    <>
      <ProductList />                                  {/* use cache: 정적 껍데기에 포함 */}
      <Suspense fallback={<p>추천 상품을 불러오는 중…</p>}>
        <PersonalizedPicks />                          {/* 쿠키 사용: 요청 시 스트리밍 */}
      </Suspense>
    </>
  )
}

async function PersonalizedPicks() {
  const segment = (await cookies()).get('segment')?.value ?? 'default'
  return <Picks segment={segment} />   // 값만 꺼내 캐시된 컴포넌트에 인자로 전달 가능
}

문서는 런타임 값을 꺼내 캐시된 함수의 인자로 넘기는 패턴도 소개한다. 이때 그 값이 캐시 키가 된다. 레이아웃 최상단에서 params를 await하면 레이아웃 전체가 프리렌더링되지 못하므로, 비동기 접근은 트리 아래쪽으로 내려 정적 껍데기를 최대한 넓힌다.

4단계: 데이터 변경 후 갱신 함수 고르기

함수동작어디서언제 쓰나
updateTag(tag)태그를 즉시 만료, 다음 읽기가 새 데이터를 기다림Server Actions 전용사용자가 자기 변경을 바로 봐야 할 때
revalidateTag(tag, 'max')stale-while-revalidate: 옛 값을 주며 백그라운드 갱신Server Functions, Route Handlers약간의 지연이 허용되는 콘텐츠(블로그, 상품 목록)
revalidateTag(tag, { expire: 0 })즉시 만료Route Handlers 등웹훅처럼 액션 밖에서 즉시 무효화가 필요할 때
revalidatePath(path)경로 단위 무효화Server Functions, Route Handlers태그를 붙이기 번거로운 단일 경로
refresh()캐시 무효화 없이 현재 경로를 다시 가져옴Server Actions캐시 밖 상태가 바뀌었을 때

revalidateTag 문서에 따르면 인자 하나만 받는 revalidateTag(tag) 형태는 더 이상 권장되지 않으며(deprecated), 두 번째 인자로 'max' 프로필을 넘기는 것이 권장 사용법이다. 액션 안에서 즉시 반영이 필요하면 updateTag로 옮긴다.

// app/products/actions.ts
'use server'
import { updateTag } from 'next/cache'

export async function updatePrice(id: string, price: number) {
  // 인증·권한·입력 검증 생략
  await db.product.update({ where: { id }, data: { price } })
  updateTag('products')     // 관리자가 저장 직후 바뀐 가격을 보게 한다
}

적용 후 확인할 것

  1. 개발 오버레이에 blocking-route, blocking-prerender 안내가 남아 있지 않은지 확인한다.
  2. 빌드 출력에서 각 경로가 정적 껍데기를 만드는지 본다.
  3. Math.random(), Date.now()를 쓰는 컴포넌트는 connection()과 Suspense로 요청 시점으로 미루거나 의도적으로 캐시한다.
  4. 데이터를 바꾸는 모든 액션이 알맞은 갱신 함수를 호출하는지 점검한다.
  5. 캐시 항목은 배포 단위로 범위가 정해져 새 배포에서 이어지지 않는다는 점을 운영 문서에 적는다.

캐시된 함수 안에서 사용자별 데이터를 다루면 다른 사용자에게 같은 결과가 보일 수 있다. 사용자 식별 값은 반드시 인자로 넘겨 캐시 키에 포함시키고, 권한 확인은 캐시 밖에서 수행한다. 서버 액션의 보안 원칙은 서버 액션 에러 핸들링 글과 함께 점검한다.

초보자가 자주 실수하는 포인트

  • 쿠키를 읽는 컴포넌트를 Suspense 없이 두어 경로 전체가 막히는 경우
  • 사용자별 데이터를 인자 없이 캐시해 다른 사용자에게 보이는 경우
  • revalidateTag(tag) 단일 인자 형태를 계속 쓰는 경우
  • 레이아웃 최상단에서 params를 await해 정적 껍데기를 줄이는 경우

체크리스트

  • next.config에 cacheComponents: true가 있는가
  • 모든 'use cache'에 cacheLife가 지정됐는가
  • 런타임 API 접근이 Suspense 안에 있는가
  • 데이터 변경 액션이 updateTag 또는 revalidateTag를 호출하는가
  • 개발 오버레이의 blocking 안내가 해소됐는가

자주 묻는 질문

use cache 결과는 서버리스 환경에서도 유지되나요?

기본 저장소는 인스턴스별 메모리라 서버리스에서는 오래 유지되지 않을 수 있다. 공유 저장소가 필요하면 use cache: remote와 캐시 핸들러를 검토한다.

updateTag를 Route Handler에서 쓸 수 있나요?

updateTag는 Server Actions 전용이다. 웹훅처럼 액션 밖에서 즉시 무효화가 필요하면 revalidateTag(tag, { expire: 0 })를 쓴다.

Suspense로 감싸면 무조건 동적 렌더링이 되나요?

아니다. Suspense는 대체 UI를 제공할 뿐이며, 동기 작업만 하는 컴포넌트는 감싸도 프리렌더링 중에 완료된다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.