테마 전환

Next.js 캐시와 revalidate 완전 정리: 데이터가 갱신되지 않을 때 해결하는 법

Easton editorial illustration: solo-founder business system console

화면의 ‘이전 데이터’를 스무 번째로 새로고침했습니다. 10분 전에 데이터베이스에서 제목을 직접 바꿨지만 페이지는 도무지 갱신되지 않습니다. 코드를 열어 보니 revalidate: 60도 분명히 적혀 있습니다. 화가 나서 지우고 revalidate: 10으로 바꿨습니다. 서버를 재시작하고 다시 새로고침해도 여전히 예전 데이터입니다.

Next.js의 캐시 메커니즘은 프레임워크 전체에서 가장 답답한 부분일 수 있습니다. 캐시 계층은 네 개이고 revalidate 방법은 세 가지이며, 14에서 15로 넘어오면서 호환성을 깨는 변경까지 생겼습니다. revalidate: 60만 설정하면 끝이라고 생각했나요? Router Cache가 방해할 수도 있고 Full Route Cache가 무효화되지 않았을 수도 있으며, 애초에 개발 환경에서 테스트하고 있을 수도 있습니다.

이 글에서는 다음 내용을 최대한 직관적으로 설명합니다.

  • Next.js 안에 캐시가 몇 계층이나 숨어 있고 각각 무엇을 담당하는지
  • revalidatePath, revalidateTag, updateTag의 차이와 선택 기준
  • 데이터가 갱신되지 않을 때 각 계층을 차례로 점검하는 방법

데이터가 갱신되지 않거나 revalidate가 동작하지 않거나 어떤 API를 써야 할지 몰라 막힌 적이 있다면, 앞으로 12분이 밤샘 몇 번을 줄여 줄지도 모릅니다.

Next.js 캐시가 복잡한 이유

왜 이렇게 많은 계층이 필요할까요?

솔직히 Next.js에 캐시 계층이 네 개나 있다는 사실을 처음 봤을 때 저도 지금 여러분과 비슷한 표정을 지었습니다. Request Memoization? Full Route Cache? 대체 무엇이고 더 단순하게 만들 수는 없는 걸까요?

차분히 생각해 보면 각 캐시 계층은 서로 다른 환경의 성능 문제를 해결합니다.

  • 컴포넌트 트리의 컴포넌트 10개가 모두 사용자 정보를 가져와야 한다고 해서 요청을 10번 보낼 수는 없습니다.
  • 블로그 글 목록은 하루에도 몇 번 갱신되지 않는데 방문할 때마다 다시 렌더링할 필요는 없습니다.
  • 사용자가 뒤로 가기 버튼을 눌렀을 때 페이지가 다시 로드될 때까지 기다리게 할 수도 없습니다.

각 계층에는 고유한 역할이 있습니다. 문제는 이 계층들이 서로 영향을 준다는 점입니다. 데이터를 바꿨는데도 어떤 캐시를 지워야 할지 알기 어려운 이유가 여기에 있습니다.

Next.js 14와 15: 캐시의 대전환

2024년 말, Next.js 15는 fetch 요청을 더 이상 기본적으로 캐시하지 않는 큰 변화를 도입했습니다.

이전(14):

fetch(url) // 기본 캐시, cache: 'force-cache'와 같음

현재(15):

fetch(url) // 기본적으로 캐시하지 않음, cache: 'no-store'와 같음

이 변화 때문에 업그레이드 후 성능이 급격히 떨어진 프로젝트가 많았습니다. 원래 자동으로 캐시되던 데이터가 이제는 전혀 캐시되지 않기 때문입니다. Vercel 포럼에서는 불만이 쏟아졌지만 공식 입장은 ‘암시적 동작보다 명시적 설정이 낫고, 캐시는 기본 동작이 아니라 개발자가 직접 선택해야 한다’는 것이었습니다.

말은 그럴듯합니다. 하지만 기존 프로젝트에는 명백히 호환성을 깨는 변경입니다.

네 가지 캐시 계층 한눈에 보기

간단히 말하면 데이터가 서버에서 사용자의 브라우저로 전달될 때 다음 네 계층을 거칩니다.

  1. Request Memoization(요청 중복 제거)
    범위: 단일 요청의 렌더링 주기
    관리자: React

  2. Data Cache(데이터 캐시)
    범위: 서버, 요청 간 영구 저장
    관리자: Next.js

  3. Full Route Cache(전체 경로 캐시)
    범위: 서버, 정적 경로
    관리자: Next.js

  4. Router Cache(라우터 캐시)
    범위: 클라이언트 브라우저 메모리
    관리자: Next.js

데이터 흐름은 대략 다음과 같습니다.

사용자 방문 → Router Cache(클라이언트) → Full Route Cache(서버)

                              Data Cache → Request Memoization → 데이터 원본

revalidate는 주로 Data Cache와 Full Route Cache에 영향을 줍니다. Router Cache는 router.refresh() 또는 강력 새로고침으로 지워야 합니다.

따라서 revalidate를 분명히 실행했는데도 클라이언트에서 페이지를 새로고침하면 예전 데이터가 보일 수 있습니다. Router Cache가 여전히 남아 있기 때문입니다.

다음 절부터 네 가지 캐시를 하나씩 살펴보면서 각 계층의 역할과 활성화·무효화 시점을 설명하겠습니다.

네 가지 캐시 계층 자세히 살펴보기

Request Memoization(요청 중복 제거)

무엇인가요?

이 기능은 Next.js 전용 기능이 아니라 React 18의 특성입니다. 한 번의 렌더링 주기 안에서 여러 컴포넌트가 동일한 GET 요청을 보내면 React가 자동으로 하나의 요청으로 합칩니다.

예를 들어 보겠습니다.

// app/page.tsx
async function UserProfile() {
  const user = await fetch('https://api.example.com/user/123')
  return <div>{user.name}</div>
}

async function UserAvatar() {
  const user = await fetch('https://api.example.com/user/123')  // 동일한 요청
  return <img src={user.avatar} />
}

export default function Page() {
  return (
    <>
      <UserProfile />
      <UserAvatar />
    </>
  )
}

두 컴포넌트 모두 같은 URL을 요청하지만 실제로는 요청을 한 번만 보냅니다. React가 첫 번째 결과를 기억했다가 두 번째 요청에서 캐시를 바로 사용합니다.

매우 짧은 수명 주기

이 캐시는 단일 렌더링 과정에서만 유효합니다. 렌더링이 끝나면 캐시도 비워집니다. 사용자가 다음에 페이지를 새로고침하면 완전히 새로운 요청이 시작됩니다.

주의 사항

  • 서버 컴포넌트(Server Component)에만 적용됩니다.
  • GET 요청에만 적용되며 POST나 PUT 요청은 메모이제이션되지 않습니다.
  • 개발 환경에서는 코드를 바꿀 때마다 다시 렌더링되므로 효과를 확인하기 어려울 수 있습니다.

언제 사용하나요?

사실 직접 신경 쓸 일이 거의 없습니다. React가 자동으로 수행하는 최적화이고 수동으로 제어할 수도 없습니다. 여러 컴포넌트에 같은 fetch를 작성해도 요청이 여러 번 나가지 않는다는 점만 알아 두면 됩니다.

Data Cache(데이터 캐시)

여기가 핵심입니다

Data Cache는 Next.js 캐시 메커니즘의 중심입니다. fetch 요청 결과를 서버 파일 시스템에 저장하며 요청, 사용자, 배포를 넘어 지속됩니다.

Next.js 14와 15의 완전히 다른 동작

Next.js 14:

fetch('https://api.example.com/posts')
// 다음과 같음
fetch('https://api.example.com/posts', { cache: 'force-cache' })
// 결과: 수동으로 revalidate하지 않는 한 데이터가 영구 캐시됨

Next.js 15:

fetch('https://api.example.com/posts')
// 다음과 같음
fetch('https://api.example.com/posts', { cache: 'no-store' })
// 결과: 매번 다시 요청하고 캐시하지 않음

데이터를 캐시하려면(Next.js 15)

방법 1: 개별 요청에 설정

fetch('https://api.example.com/posts', {
  cache: 'force-cache',
  next: { revalidate: 3600 }  // 1시간 후 재검증
})

방법 2: 경로 전체에 설정

// app/blog/page.tsx
export const revalidate = 3600

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  // ...
}

언제 무효화되나요?

Data Cache가 무효화되는 시점은 세 가지입니다.

  1. 설정한 revalidate 시간이 지났을 때(예: 3,600초)
  2. revalidatePath() 또는 revalidateTag()를 수동으로 호출했을 때
  3. 애플리케이션을 다시 배포했을 때

여러 fetch의 revalidate 시간이 서로 다르면 어떻게 되나요?

한 페이지에 fetch가 여러 개 있고 각각의 revalidate 시간이 다르면 Next.js는 가장 짧은 시간을 페이지 전체의 재검증 시간으로 사용합니다.

async function Page() {
  const posts = await fetch('...', { next: { revalidate: 60 } })    // 60초
  const user = await fetch('...', { next: { revalidate: 3600 } })  // 1시간

  // 실제로 전체 페이지는 60초마다 재검증됨
}

Full Route Cache(전체 경로 캐시)

HTML 수준의 캐시

Data Cache가 데이터를 캐시한다면 Full Route Cache는 페이지 전체의 HTML과 RSC Payload(React Server Component의 직렬화 데이터)를 캐시합니다.

언제 캐시되나요?

정적으로 렌더링되는 경로만 캐시됩니다. 정적 렌더링은 빌드할 때 콘텐츠를 확정할 수 있는 페이지를 뜻합니다.

반대로 페이지에서 다음 기능을 사용하면 동적 렌더링으로 전환되어 캐시되지 않습니다.

  • cookies()
  • headers()
  • searchParams
  • 불안정한 함수(예: Math.random(), Date.now())

페이지가 정적인지 동적인지 어떻게 확인하나요?

npm run build를 실행하면 터미널에 다음과 같은 정보가 표시됩니다.

Route (app)                              Size     First Load JS
┌ ○ /                                    5 kB           87 kB
├ ● /blog                                1 kB           88 kB
└ ƒ /api/user                            0 kB           87 kB

○  (Static)  정적 HTML로 자동 렌더링(동적 데이터 없음)
●  (SSG)     정적 HTML + JSON 자동 생성(getStaticProps 사용)
ƒ  (Dynamic) 서버에서 요청 시 렌더링

또는 이면 정적 경로라서 캐시됩니다. ƒ이면 동적 경로라서 캐시되지 않습니다.

정적 또는 동적 렌더링 강제하기

// 정적 렌더링 강제
export const dynamic = 'force-static'

// 동적 렌더링 강제
export const dynamic = 'force-dynamic'

언제 무효화되나요?

Full Route Cache가 무효화되는 시점은 다음과 같습니다.

  1. Data Cache가 무효화될 때 Full Route Cache도 무효화됩니다. 데이터가 바뀌면 페이지도 다시 렌더링해야 하기 때문입니다.
  2. revalidatePath('/blog')를 호출했을 때
  3. 애플리케이션을 다시 배포했을 때

Router Cache(라우터 캐시)

클라이언트 쪽의 작은 요령

Router Cache는 사용자의 브라우저 메모리에 존재합니다. 사용자가 한 번 방문한 페이지의 콘텐츠를 클라이언트에 저장해 두었다가, 다음에 뒤로 가기 버튼을 누르거나 같은 페이지로 이동하면 서버에 다시 요청하지 않고 캐시를 바로 사용합니다.

자동 프리패치도 있습니다

<Link href="/about">를 사용하면 링크가 사용자의 뷰포트에 들어오는 순간 Next.js가 /about 페이지의 콘텐츠를 자동으로 프리패치해 Router Cache에 저장합니다. 사용자가 실제로 클릭할 때는 즉시 이동할 수 있습니다.

수명 주기(Next.js 14)

  • 정적 경로: 5분 캐시
  • 동적 경로: 30초 캐시

Next.js 15의 변화

Next.js 15는 기본적으로 Router Cache를 활성화하지 않습니다. 또는 캐시 시간이 매우 짧다고 볼 수 있습니다. 사용하려면 next.config.js에서 설정해야 합니다.

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30,      // 동적 경로 30초 캐시
      static: 180,      // 정적 경로 180초 캐시
    },
  },
}

언제 무효화되나요?

  • 캐시 시간이 지났을 때
  • 사용자가 강력 새로고침을 했을 때(Ctrl+Shift+R)
  • router.refresh()를 호출했을 때

revalidate가 동작하지 않는 것처럼 보이는 이유

서버에서 revalidatePath를 호출해 데이터가 실제로 갱신됐는데도 사용자에게는 여전히 예전 데이터가 보일 때가 많습니다. 대부분 Router Cache가 아직 무효화되지 않았기 때문입니다.

해결 방법은 다음과 같습니다.

  1. 사용자가 강력 새로고침을 합니다. 하지만 사용자에게 이를 요구할 수는 없습니다.
  2. 데이터를 갱신한 뒤 클라이언트 컴포넌트에서 router.refresh()를 호출합니다.
  3. Router Cache 시간을 줄입니다.

revalidate 방법 완전 정리

앞에서 네 가지 캐시 계층의 원리를 살펴봤습니다. 이제 가장 실용적인 부분인 캐시를 무효화하는 방법을 알아보겠습니다.

Next.js는 여러 revalidate 방법을 제공하며 각각 적합한 상황이 다릅니다. 차이를 정확히 알면 디버깅 시간을 크게 줄일 수 있습니다.

시간 기반 revalidate(Time-based)

가장 자주 쓰는 방법

시간 기반 revalidate는 ‘X초마다 데이터를 자동으로 다시 가져오기’입니다. ISR(Incremental Static Regeneration, 증분 정적 재생성)의 핵심 메커니즘입니다.

두 가지 작성 방법

방법 1: fetch 요청에 설정

const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }  // 3600초 = 1시간
})

방법 2: 경로 파일 최상단에 설정

// app/blog/page.tsx
export const revalidate = 3600

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  return <PostList posts={posts} />
}

동작 원리(ISR)

revalidate: 3600을 설정했다고 가정해 보겠습니다.

  1. 첫 번째 사용자가 페이지 방문 → 정적 HTML 생성 후 1시간 캐시
  2. 이후 1시간 동안 모든 사용자에게 캐시된 HTML 반환(매우 빠름)
  3. 1시간 후 다음 사용자가 방문 → 여전히 이전 HTML 반환(기다리지 않음)
  4. 동시에 Next.js가 백그라운드에서 새 HTML 생성
  5. 새 HTML 생성이 끝나면 이후 사용자에게 새 콘텐츠 반환

이 메커니즘을 stale-while-revalidate라고 합니다. 사용자는 기다릴 필요가 없지만 항상 한 명은 만료된 데이터를 보게 된다는 단점이 있습니다.

적합한 상황

  • 블로그 글 목록(한 시간에 몇 번 갱신)
  • 뉴스 홈페이지(30분마다 갱신)
  • 제품 카탈로그(하루에 한 번 갱신)

자주 발생하는 문제 1: 개발 환경에서 동작하지 않음

revalidate를 설정했는데 동작하지 않는다는 불만이 많습니다. 가장 먼저 개발 환경에서 테스트하는지 프로덕션 환경에서 테스트하는지 확인해야 합니다.

개발 환경(npm run dev)에서는 Next.js가 대부분의 캐시를 비활성화하고 요청마다 다시 렌더링합니다. 반드시 프로덕션 환경에서 테스트해야 합니다.

npm run build
npm start

자주 발생하는 문제 2: 여러 fetch의 시간이 서로 다름

앞에서 설명했듯 한 페이지에 revalidate 시간이 다른 fetch가 여러 개 있으면 Next.js는 최솟값을 사용합니다. 다만 Data Cache는 각 fetch의 개별 시간도 그대로 존중합니다.

async function Page() {
  // 이 요청은 60초마다 재검증
  const posts = await fetch('...', { next: { revalidate: 60 } })

  // 이 요청은 3600초마다 재검증
  const user = await fetch('...', { next: { revalidate: 3600 } })
}

페이지는 60초마다 다시 렌더링되지만 user 요청의 데이터 캐시는 1시간 동안 유지됩니다. 즉, 첫 1시간 동안에는 페이지가 60초마다 다시 렌더링되어도 user 데이터는 바뀌지 않으며 1시간이 지나야 갱신됩니다.

조금 복잡하게 들리지만 실제로는 합리적인 설계입니다.

온디맨드 revalidate: revalidatePath

사용자 동작으로 유발되는 갱신

시간 기반 revalidate가 타이머라면 revalidatePath는 버튼과 같습니다. 사용자가 새 글을 게시하는 등 특정 이벤트가 발생했을 때 캐시 무효화를 수동으로 실행합니다.

사용 방법

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function publishPost(formData) {
  // 글 게시 로직
  await db.posts.create({ ... })

  // 블로그 목록 페이지 캐시 무효화
  revalidatePath('/blog')
}

그런 다음 클라이언트 컴포넌트에서 호출합니다.

// app/components/PublishButton.tsx
'use client'

import { publishPost } from '@/app/actions'

export function PublishButton() {
  return (
    <form action={publishPost}>
      <button type="submit">글 게시</button>
    </form>
  )
}

경로 유형 지정하기

revalidatePath에는 경로 유형을 지정할 수 있습니다.

// /blog 페이지만 재검증
revalidatePath('/blog', 'page')

// /blog 아래의 모든 페이지 재검증(/blog/post-1, /blog/post-2 포함)
revalidatePath('/blog', 'layout')

중요: 즉시 다시 생성되는 것은 아닙니다

revalidatePath를 호출하면 페이지가 즉시 다시 생성된다고 생각하는 사람이 많지만 그렇지 않습니다.

revalidatePath는 캐시를 무효 상태로 표시할 뿐입니다. 실제 재생성은 다음 사용자가 해당 페이지를 방문할 때 일어납니다.

  1. revalidatePath('/blog') 호출
  2. 캐시 삭제
  3. 다음 사용자가 /blog 방문 → 이때 HTML 재생성(사용자는 기다려야 함)
  4. 이후 사용자에게 새 콘텐츠 반환

적합한 상황

  • 사용자가 새 콘텐츠를 게시한 뒤 목록 갱신
  • 관리자가 설정을 바꾼 뒤 관련 페이지 갱신
  • 사용자가 폼을 제출한 뒤 현재 페이지 갱신

온디맨드 revalidate: revalidateTag

더 유연한 무효화 제어

revalidatePath가 경로 단위로 무효화한다면 revalidateTag태그 단위로 무효화합니다. 데이터에 태그를 붙인 뒤 그 태그를 사용하는 모든 캐시를 한꺼번에 무효화할 수 있습니다.

사용 방법

1단계: fetch 요청에 태그 지정

const posts = await fetch('https://api.example.com/posts', {
  next: {
    revalidate: 3600,
    tags: ['posts']  // 'posts' 태그 지정
  }
})

const authors = await fetch('https://api.example.com/authors', {
  next: {
    revalidate: 3600,
    tags: ['posts', 'authors']  // 태그를 여러 개 지정할 수 있음
  }
})

2단계: 특정 태그의 캐시 무효화

'use server'

import { revalidateTag } from 'next/cache'

export async function publishPost() {
  await db.posts.create({ ... })

  // 'posts' 태그가 지정된 모든 데이터 무효화
  revalidateTag('posts')
}

profile=“max”의 stale-while-revalidate 전략

Next.js 15는 profile="max" 사용을 권장합니다.

revalidateTag('posts', { profile: 'max' })

profile="max"를 사용하면 다음과 같이 동작합니다.

  1. 만료 상태로 표시하지만 캐시를 즉시 삭제하지는 않음
  2. 다음 사용자 방문 → 이전 데이터를 빠르게 반환
  3. 동시에 백그라운드에서 새 데이터 가져오기
  4. 새 데이터가 준비되면 이후 요청에 새 데이터 반환

사용자가 기다릴 필요가 없으므로 기본 동작보다 낫습니다.

revalidatePath와 revalidateTag의 차이

비교 항목revalidatePathrevalidateTag
무효화 단위경로데이터 태그
여러 페이지 지원구체적인 경로만 지정 가능여러 페이지에 걸쳐 적용 가능
세밀함낮음높음
사용 난이도간단함태그를 미리 설계해야 함

Tag는 언제 사용하나요?

여러 페이지에서 같은 데이터를 사용할 때는 Tag가 더 적합합니다.

블로그 시스템을 예로 들면 다음 네 페이지가 모두 ‘글 데이터’를 사용합니다.

  • 글 목록 페이지 /blog
  • 글 상세 페이지 /blog/[slug]
  • 작성자 페이지 /author/[id]
  • 홈페이지의 ‘최신 글’ 영역

revalidatePath를 사용하면 다음을 모두 호출해야 합니다.

revalidatePath('/blog')
revalidatePath('/blog/[slug]')
revalidatePath('/author/[id]')
revalidatePath('/')

반면 글 데이터에 posts 태그를 붙이면 한 줄이면 됩니다.

revalidateTag('posts')

이 태그를 사용하는 모든 페이지가 무효화되므로 훨씬 간단합니다.

새 기능: updateTag(Next.js 15)

지연 무효화가 아닌 즉시 무효화

updateTag는 Next.js 15에서 추가된 API입니다. revalidateTag와 달리 캐시를 만료 상태로 표시하는 대신 즉시 삭제합니다.

'use server'

import { updateTag } from 'next/cache'

export async function updateUserProfile(userId, newData) {
  await db.users.update({ where: { id: userId }, data: newData })

  // 사용자 데이터 캐시 즉시 무효화
  updateTag(`user-${userId}`)
}

revalidateTag와의 차이

비교 항목revalidateTagupdateTag
무효화 방식만료 상태로 표시하고 다음 방문 시 백그라운드 갱신캐시 즉시 삭제
다음 방문이전 데이터를 반환하고 백그라운드에서 새 데이터 가져오기차단 상태로 새 데이터를 기다림
사용 제한어디서든 사용 가능Server Actions 안에서만 사용 가능
적합한 상황속도를 우선하는 일반 상황‘자신이 쓴 값을 바로 읽어야 하는’ 상황

‘자신이 쓴 값을 바로 읽기’란 무엇인가요?

사용자가 프로필 페이지에서 닉네임을 바꾸고 저장했다면 페이지에는 즉시 새 닉네임이 보여야 합니다. 이전 닉네임을 보여 준 뒤 백그라운드 갱신을 기다리게 해서는 안 됩니다.

이런 상황에서 updateTag를 사용합니다.

export async function updateProfile(formData) {
  const userId = getCurrentUserId()

  await db.users.update({
    where: { id: userId },
    data: { nickname: formData.get('nickname') }
  })

  // 즉시 무효화해 다음 읽기에서 최신 데이터 보장
  updateTag(`user-${userId}`)

  revalidatePath('/profile')
}

새 기능: use cache 지시어(Next.js 15)

명시적으로 캐시 선언하기

Next.js 15는 캐시할 함수를 명시적으로 표시할 수 있는 새 'use cache' 지시어를 도입했습니다.

사용 방법

'use cache'

export async function getPopularPosts() {
  const posts = await db.posts.findMany({
    orderBy: { views: 'desc' },
    take: 10
  })
  return posts
}

cacheTag와 함께 사용하기

import { unstable_cacheTag as cacheTag } from 'next/cache'

'use cache'

export async function getPostsByAuthor(authorId) {
  cacheTag('posts', `author-${authorId}`)

  return await db.posts.findMany({
    where: { authorId }
  })
}

그런 다음 revalidateTag로 무효화할 수 있습니다.

revalidateTag(`author-${authorId}`)

왜 필요한가요?

Next.js 15에서는 fetch가 기본적으로 캐시되지 않습니다. 데이터베이스를 매번 다시 조회하고 싶지 않다면 캐시를 명시적으로 선언해야 합니다. use cache를 쓰면 의도가 더 분명해집니다.

자주 발생하는 문제 해결 가이드

이론은 끝났습니다. 이제 가장 실용적인 부분인 캐시 문제가 생겼을 때의 진단 방법을 살펴보겠습니다.

가장 흔한 네 가지 문제를 골라 각각의 진단 단계와 해결책을 설명합니다.

문제 1: revalidate를 설정했지만 동작하지 않음

증상

export const revalidate = 60을 설정했는데 10분 뒤 페이지에 접속해도 데이터가 그대로입니다.

진단 단계

1. 프로덕션 환경에서 테스트하는지 확인

가장 흔한 오해입니다. 개발 환경(npm run dev)에서는 대부분의 캐시가 비활성화됩니다.

다음과 같이 테스트해야 합니다.

npm run build
npm start

2. 경로가 동적으로 렌더링되는지 확인

npm run build를 실행하고 터미널 출력을 확인합니다.

Route (app)                Size
├ ○ /blog                  1 kB    ← 정적, 캐시됨
└ ƒ /profile               2 kB    ← 동적, 캐시되지 않음

경로가 ƒ(동적)라면 동적 경로는 캐시되지 않으므로 revalidate가 전혀 적용되지 않습니다.

동적 렌더링을 유발할 수 있는 코드:

// 다음 코드는 모두 경로를 동적 렌더링으로 전환함
import { cookies } from 'next/headers'
import { headers } from 'next/headers'

export default function Page({ searchParams }) {  // searchParams 사용
  const cookieStore = cookies()  // cookies 사용
  // ...
}

해결책:

  • 동적 렌더링이 필요 없다면 해당 코드를 제거합니다.
  • 동적 렌더링이 필요하다면 revalidate가 적용될 것으로 기대하지 말고 온디맨드 revalidate를 사용합니다.

3. Next.js 버전 확인

Next.js 14와 15의 기본 동작은 다릅니다. 방금 15로 업그레이드했다면 이전에 캐시되던 많은 항목이 이제 캐시되지 않을 수 있습니다.

해결책(Next.js 15):

// 캐시를 명시적으로 활성화
fetch(url, {
  cache: 'force-cache',
  next: { revalidate: 60 }
})

// 또는 use cache 지시어 사용
'use cache'
export async function getData() {
  // ...
}

4. Router Cache가 방해하는지 확인

서버 데이터가 갱신됐더라도 클라이언트의 Router Cache가 이전 데이터를 저장하고 있을 수 있습니다.

해결책:

  • 강력 새로고침(Ctrl+Shift+R)
  • Next.js 15에서 더 짧은 staleTimes 설정

문제 2: 데이터는 갱신됐지만 페이지에는 이전 데이터가 표시됨

증상

데이터베이스에서 직접 데이터를 바꾸거나 revalidatePath를 호출했는데 페이지를 새로고침해도 이전 데이터가 보입니다.

진단 방향: 네 가지 캐시 계층을 순서대로 확인

첫 번째 계층: Router Cache(클라이언트)

가장 놓치기 쉬운 부분이 클라이언트 캐시입니다.

빠른 테스트:

  • Ctrl+Shift+R로 강력 새로고침합니다.
  • 데이터가 갱신됐다면 Router Cache 문제입니다.

해결책:

'use client'

import { useRouter } from 'next/navigation'

export function RefreshButton() {
  const router = useRouter()

  return (
    <button onClick={() => router.refresh()}>
      새로고침
    </button>
  )
}

또는 next.config.js에서 더 짧은 캐시 시간을 설정합니다.

module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 0,    // 동적 경로 캐시 비활성화
      static: 30,    // 정적 경로 30초 캐시
    },
  },
}

두 번째 계층: Full Route Cache(서버)

경로가 정적인지 확인합니다. 정적 경로라면 HTML 전체가 캐시됩니다.

빠른 테스트:

# 새 시크릿 창에서 페이지 열기
# 여전히 이전 데이터가 보이면 서버 캐시 문제

해결책:

'use server'

import { revalidatePath } from 'next/cache'

export async function updateData() {
  await db.update({ ... })

  // 경로 캐시 무효화
  revalidatePath('/your-page')
}

세 번째 계층: Data Cache(서버)

fetch 요청 설정을 확인합니다.

빠른 테스트:

fetch에 타임스탬프 로그를 추가합니다.

const data = await fetch(url)
console.log('Fetched at:', new Date().toISOString())

페이지를 새로고침했는데 타임스탬프가 바뀌지 않으면 캐시를 사용하고 있다는 뜻입니다.

해결책:

방법 1: 데이터에 태그를 지정한 뒤 무효화

// 데이터를 가져올 때
const data = await fetch(url, {
  next: { tags: ['my-data'] }
})

// 데이터를 갱신할 때
revalidateTag('my-data')

방법 2: 테스트를 위해 일시적으로 캐시 비활성화

const data = await fetch(url, {
  cache: 'no-store'  // 캐시하지 않음
})

네 번째 계층: Request Memoization(서버)

이 계층은 단일 요청에서만 유효하므로 일반적으로 문제를 일으키지 않습니다. 앞의 세 계층에 문제가 없다면 캐시가 아니라 데이터 원본 자체의 문제일 수 있습니다.

문제 3: revalidatePath와 revalidateTag 중 무엇을 선택해야 하나요?

결정 트리

캐시를 무효화해야 함
    |
    ├─ 한 페이지만 영향을 받음
    |      → revalidatePath('/specific-page') 사용
    |
    ├─ 한 경로 아래의 모든 페이지가 영향을 받음
    |      → revalidatePath('/blog', 'layout') 사용
    |
    ├─ 여러 경로의 페이지가 같은 데이터를 사용함
    |      → revalidateTag('your-tag') 사용
    |
    └─ 즉시 무효화해야 함(사용자가 편집 후 바로 확인)
           → updateTag('your-tag') 사용(Next.js 15)

실전 사례: 블로그 시스템

블로그 시스템에 다음 페이지가 있다고 가정해 보겠습니다.

  • 홈페이지: 최신 글 3개 표시
  • 블로그 목록 페이지 /blog: 모든 글 표시
  • 글 상세 페이지 /blog/[slug]: 글 하나 표시
  • 작성자 페이지 /author/[id]: 해당 작성자의 모든 글 표시

태그 사용 전략:

// 데이터를 가져올 때 태그 지정
async function getPosts() {
  return fetch('https://api.example.com/posts', {
    next: {
      revalidate: 3600,
      tags: ['posts']  // 모든 글 관련 데이터에 이 태그 지정
    }
  })
}

async function getPostBySlug(slug) {
  return fetch(`https://api.example.com/posts/${slug}`, {
    next: {
      revalidate: 3600,
      tags: ['posts', `post-${slug}`]  // 공통 태그와 개별 태그를 함께 지정
    }
  })
}

async function getPostsByAuthor(authorId) {
  return fetch(`https://api.example.com/posts?author=${authorId}`, {
    next: {
      revalidate: 3600,
      tags: ['posts', `author-${authorId}-posts`]
    }
  })
}

새 글을 게시할 때:

export async function publishPost(formData) {
  await db.posts.create({ ... })

  // 'posts' 태그만 무효화하면 됨
  // 이 태그를 사용하는 모든 페이지가 갱신됨
  revalidateTag('posts')
}

특정 글을 수정할 때:

export async function updatePost(slug, newData) {
  await db.posts.update({ where: { slug }, data: newData })

  // 이 글과 관련된 캐시만 무효화
  revalidateTag(`post-${slug}`)

  // 목록 페이지도 함께 갱신하려면 다음 호출 추가
  revalidateTag('posts')
}

문제 4: Next.js 14에서 15로 업그레이드한 뒤 캐시가 동작하지 않음

증상

Next.js 15로 업그레이드한 뒤 예전에는 잘 캐시되던 데이터가 요청할 때마다 다시 로드되고 성능이 급격히 떨어집니다.

원인

Next.js 15에서는 세 가지 기본 캐시 동작이 바뀌었습니다.

  1. fetch의 기본값이 force-cache에서 no-store로 변경됨
  2. GET 경로 처리기가 기본적으로 캐시되지 않음
  3. Router Cache가 기본적으로 활성화되지 않음

마이그레이션 방법

방법 1: 캐시를 명시적으로 활성화(권장)

// 이전(Next.js 14)
const data = await fetch(url)

// 현재(Next.js 15) - 명시적으로 선언해야 함
const data = await fetch(url, {
  cache: 'force-cache',
  next: { revalidate: 3600 }
})

방법 2: use cache 지시어 사용

'use cache'

export async function getPostList() {
  const posts = await db.posts.findMany()
  return posts
}

방법 3: Router Cache 활성화

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
}

마이그레이션 전후 비교:

// Next.js 14 - 암시적 캐시
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts')
  // 자동 캐시
}

// Next.js 15 - 명시적 캐시
'use cache'  // 이 줄 추가

export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    cache: 'force-cache',  // 또는 이 옵션 추가
    next: { revalidate: 3600 }
  })
}

권장 사항:

한 번에 자동으로 마이그레이션되기를 기대하지 마세요. 각 데이터 요청을 꼼꼼히 검토하고 어떤 데이터를 캐시할지 의식적으로 결정해야 합니다. 번거롭지만 장기적으로는 더 건강한 선택입니다. 시스템 안에서 어떤 데이터가 캐시되고 어떤 데이터가 캐시되지 않는지 분명히 알 수 있기 때문입니다.

모범 사례와 선택 전략

지금까지 살펴본 내용을 바탕으로 언제 어떤 캐시 전략을 사용해야 하는지 정리해 보겠습니다.

캐시 전략 선택 흐름도

데이터가 얼마나 자주 갱신되나요?
    |
    ├─ 거의 변하지 않음(예: 소개 페이지, 도움말 문서)
    |      → 정적 생성, revalidate 설정하지 않음
    |      → 변경 시 수동으로 재배포
    |
    ├─ 정기적으로 갱신(예: 매시간, 매일)
    |      → ISR + 시간 기반 revalidate
    |      → export const revalidate = 3600
    |
    ├─ 불규칙하게 갱신(예: 사용자가 콘텐츠 게시)
    |      → 온디맨드 revalidate
    |      → revalidatePath 또는 revalidateTag
    |
    └─ 실시간 갱신(예: 채팅, 실시간 데이터)
           → 동적 렌더링 + cache: 'no-store'
           → 캐시하지 않음

태그 이름 지정 전략

revalidateTag를 사용한다면 다음과 같은 이름 규칙을 권장합니다.

세분화 수준 설계

  • 큰 단위(일괄 무효화에 적합)

    • posts - 모든 글
    • products - 모든 제품
    • users - 모든 사용자
  • 중간 단위(카테고리 또는 상태별)

    • posts:published - 게시된 글
    • posts:draft - 초안
    • products:category:electronics - 전자제품 카테고리
  • 작은 단위(구체적인 리소스)

    • post:id:123 - ID가 123인 글
    • user:profile:456 - ID가 456인 사용자 프로필

권장 이름 규칙

네임스페이스를 사용해 entity:type:id 형식으로 작성합니다.

// 데이터를 가져올 때
const post = await fetch(`/api/posts/${id}`, {
  next: {
    tags: [
      'posts',                    // 큰 단위
      'posts:published',          // 중간 단위
      `post:id:${id}`             // 작은 단위
    ]
  }
})

// 무효화할 때 필요한 단위를 선택
revalidateTag('posts')              // 모든 글 무효화
revalidateTag('posts:published')    // 게시된 글만 무효화
revalidateTag(`post:id:${id}`)      // 특정 글만 무효화

성능 최적화 권장 사항

1. 지나치게 캐시하지 않기

캐시가 많을수록 좋은 것은 아닙니다. 너무 많이 캐시하면 다음과 같은 문제가 생깁니다.

  • 데이터 불일치
  • 어려운 디버깅
  • 저장 공간 낭비

경험칙:

  • 사용자 개인 데이터(예: 장바구니, 개인 설정) → 캐시하지 않음
  • 공개 데이터(예: 상품 목록, 글 목록) → 캐시
  • 실시간 데이터(예: 재고, 온라인 사용자 수) → 캐시하지 않거나 매우 짧게 캐시

2. 적절한 revalidate 시간 설정하기

시간을 지나치게 짧게 설정하지 마세요. revalidate: 1로 설정하면 매초 페이지가 다시 생성될 수 있으므로 캐시의 의미가 사라집니다.

권장 설정:

  • 뉴스: 30~60분
  • 블로그 글: 1~2시간
  • 제품 카탈로그: 2~4시간
  • 정적 페이지: 24시간 이상

3. stale-while-revalidate 사용하기

Next.js 15의 profile="max"가 바로 이 전략입니다.

revalidateTag('posts', { profile: 'max' })

사용자에게는 항상 캐시된 콘텐츠를 빠르게 보여 주고 시스템은 백그라운드에서 조용히 캐시를 갱신합니다. 사용자 경험 측면에서 가장 좋은 방식입니다.

4. 캐시 적중률 모니터링하기

.env.local에 다음 값을 추가합니다.

NEXT_PRIVATE_DEBUG_CACHE=1

그런 다음 프로덕션 서버를 실행하면 콘솔에 캐시 적중 상태가 표시됩니다.

○ GET /blog 200 in 45ms (cache: HIT)
○ GET /about 200 in 12ms (cache: SKIP)

캐시 전략이 효과적인지 정기적으로 확인하세요.

개발 환경과 프로덕션 환경의 차이

핵심 주의 사항:

개발 환경(npm run dev)과 프로덕션 환경(npm start)의 캐시 동작은 완전히 다릅니다.

특성개발 환경프로덕션 환경
Data Cache대부분 비활성화완전히 활성화
Full Route Cache비활성화정적 경로에서 활성화
Request Memoization활성화활성화
Router Cache활성화되지만 시간이 매우 짧음전체 시간 적용

캐시 기능을 올바르게 테스트하는 방법:

# 1. 프로덕션 버전 빌드
npm run build

# 2. 터미널 출력을 보고 경로 유형 확인
#    ○ = 정적, 캐시됨
#    ƒ = 동적, 캐시되지 않음

# 3. 프로덕션 서버 시작
npm start

# 4. 캐시 동작 테스트
# 브라우저에서 페이지를 연 뒤 데이터 원본 수정
# 페이지를 새로고침하고 이전 데이터가 남아 있는지 확인

# 5. revalidate 테스트
# revalidate 시간이 지난 뒤 다시 접속해 갱신 여부 확인

개발 환경에서 캐시 문제를 디버깅하지 마세요. 가장 흔한 함정입니다. 개발 환경에서 한참 씨름한 뒤 revalidate가 동작하지 않는다고 결론 내리는 경우가 너무 많습니다.

결론

내용이 많았지만 핵심은 몇 가지뿐입니다.

1. 네 가지 캐시 계층의 역할 이해하기

서로 혼동하지 마세요. Router Cache는 클라이언트에 있고 나머지 세 가지는 서버에 있습니다. revalidate는 주로 Data Cache와 Full Route Cache에 영향을 줍니다.

2. 적절한 revalidate 방법 선택하기

  • 정기 갱신export const revalidate = 3600
  • 사용자 동작 후 단일 페이지 갱신revalidatePath('/page')
  • 사용자 동작 후 여러 페이지 갱신revalidateTag('tag')
  • 즉시 무효화updateTag('tag')(Next.js 15)

3. 프로덕션 환경에서 테스트하기

개발 환경의 캐시 동작은 정확하지 않다는 점을 항상 기억하세요. 캐시를 테스트하려면 반드시 npm run build && npm start를 사용해야 합니다.

4. 계층별로 문제 진단하기

데이터가 갱신되지 않으면 다음 순서로 확인합니다.

  1. Router Cache(클라이언트) → 강력 새로고침 시도
  2. Full Route Cache(서버) → revalidatePath
  3. Data Cache(서버) → revalidateTag
  4. 데이터 원본 자체

5. 암시적 동작보다 명시적 설정(Next.js 15의 철학)

Next.js 15는 캐시를 기본 활성화에서 기본 비활성화로 바꿨습니다. 이제 어떤 데이터를 캐시할지 능동적으로 판단해야 합니다. 번거롭지만 장기적으로는 코드를 더 명확하고 유지보수하기 쉽게 만듭니다.


여기까지 읽었다면 Next.js 캐시 메커니즘을 전체적으로 이해한 것입니다. 다음에 데이터가 갱신되지 않는 문제를 만나도 어디서부터 확인해야 할지 알 수 있을 겁니다.

캐시는 분명 복잡하지만 제대로 이해하면 Next.js의 가장 강력한 기능 중 하나입니다. 적절히 사용하면 애플리케이션이 눈에 띄게 빨라지고, 무분별하게 사용하면 스스로 함정을 파게 됩니다.

애플리케이션은 빠르고 버그는 없기를 바랍니다!

Next.js 캐시를 이해하고 적용하는 전체 절차

네 가지 캐시 계층을 이해하고 적절한 revalidate 방법을 선택한 뒤 데이터 미갱신 문제를 해결하는 절차

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Next.js의 네 가지 캐시 계층 이해하기

    네 가지 캐시 계층:

    1. Request Memoization(요청 중복 제거)
    • 한 요청 안에서 같은 URL에는 요청을 한 번만 보냅니다.
    • 자동으로 처리되며 설정이 필요 없습니다.
    • 수명 주기: 단일 요청

    2. Data Cache(fetch 캐시)
    • fetch 요청 결과를 캐시합니다.
    • 기본 동작: Next.js 14는 캐시하고 15는 캐시하지 않습니다.
    • 설정: cache 옵션

    3. Full Route Cache(전체 경로 캐시)
    • 전체 경로의 HTML을 캐시합니다.
    • 정적 페이지는 자동으로 캐시됩니다.
    • 설정: revalidate

    4. Router Cache(클라이언트 라우터 캐시)
    • 클라이언트 탐색 시 사용하는 캐시입니다.
    • 자동으로 처리되며 설정이 필요 없습니다.
    • 수명 주기: 세션 동안

    핵심: Router Cache는 클라이언트에 있고 나머지 세 가지는 서버에 있습니다.
  2. 2

    Step 2: 적절한 revalidate 방법 선택하기

    세 가지 방법:

    1. 정기 갱신(export const revalidate)
    ```tsx
    export const revalidate = 3600 // 3600초 후 재검증
    ```
    • 용도: 정기적으로 갱신하는 콘텐츠
    • 설정 위치: page.tsx 또는 layout.tsx

    2. 사용자가 유발하는 단일 페이지 갱신(revalidatePath)
    ```tsx
    import { revalidatePath } from 'next/cache'

    revalidatePath('/blog/post-1')
    ```
    • 용도: 사용자 작업 후 단일 페이지 갱신
    • 사용 위치: Server Action 또는 API Route

    3. 사용자가 유발하는 여러 페이지 갱신(revalidateTag)
    ```tsx
    import { revalidateTag } from 'next/cache'

    // fetch할 때 태그 추가
    fetch(url, { next: { tags: ['posts'] } })

    // 갱신할 때 태그 무효화
    revalidateTag('posts')
    ```
    • 용도: 사용자 작업 후 여러 페이지 갱신
    • 사용 위치: Server Action 또는 API Route

    선택 기준:
    • 정기 갱신 → export const revalidate
    • 단일 페이지 → revalidatePath
    • 여러 페이지 → revalidateTag
  3. 3

    Step 3: 데이터가 갱신되지 않는 문제 진단하기

    진단 순서:

    1. 개발 환경인지 확인
    • 개발 환경의 캐시 동작은 정확하지 않습니다.
    • 반드시 프로덕션 환경에서 테스트합니다: npm run build && npm start

    2. Router Cache(클라이언트) 확인
    • 강력 새로고침을 시도합니다(Ctrl+Shift+R).
    • 브라우저 캐시를 삭제합니다.

    3. Full Route Cache(서버) 확인
    • revalidatePath로 무효화합니다.
    • revalidate 설정을 확인합니다.

    4. Data Cache(서버) 확인
    • revalidateTag로 무효화합니다.
    • fetch의 cache 설정을 확인합니다.

    5. 데이터 원본 확인
    • 데이터 원본이 실제로 갱신됐는지 확인합니다.
    • API 응답 데이터를 확인합니다.

    자주 발생하는 문제:
    • 개발 환경에서 테스트함 → 프로덕션 환경 사용
    • Router Cache가 남아 있음 → 강력 새로고침
    • revalidate 설정 오류 → 설정 확인
    • Next.js 15는 기본적으로 캐시하지 않음 → cache를 명시적으로 설정
  4. 4

    Step 4: Next.js 14와 15의 캐시 차이 확인하기

    Next.js 14:
    • fetch를 기본으로 캐시합니다(getStaticProps와 같은 동작).
    • 비활성화하려면 cache: 'no-store'를 명시합니다.

    Next.js 15:
    • fetch를 기본적으로 캐시하지 않습니다.
    • 활성화하려면 cache: 'force-cache'를 명시합니다.

    마이그레이션 권장 사항:
    • 모든 fetch 호출을 확인합니다.
    • cache 옵션을 명시합니다.
    • 캐시 동작을 테스트합니다.

    코드 예시:
    ```tsx
    // Next.js 14(기본 캐시)
    fetch(url) // 자동 캐시

    // Next.js 15(기본적으로 캐시하지 않음)
    fetch(url, { cache: 'force-cache' }) // 명시적 설정 필요
    ```

    핵심: Next.js 15의 철학은 ‘암시적 동작보다 명시적 설정’입니다. 어떤 데이터를 캐시할지 개발자가 직접 판단해야 합니다.

FAQ

Next.js에는 몇 가지 캐시 계층이 있고 각각 무엇을 담당하나요?
네 가지 캐시 계층이 있습니다.

1. Request Memoization(요청 중복 제거)
• 한 요청 안에서 같은 URL에는 요청을 한 번만 보냅니다.
• 자동 처리되며 설정이 필요 없습니다.
• 수명 주기: 단일 요청

2. Data Cache(fetch 캐시)
• fetch 요청 결과를 캐시합니다.
• 기본 동작: Next.js 14는 캐시하고 15는 캐시하지 않습니다.
• 설정: cache 옵션

3. Full Route Cache(전체 경로 캐시)
• 전체 경로의 HTML을 캐시합니다.
• 정적 페이지는 자동으로 캐시됩니다.
• 설정: revalidate

4. Router Cache(클라이언트 라우터 캐시)
• 클라이언트 탐색 시 사용하는 캐시입니다.
• 자동 처리되며 설정이 필요 없습니다.
• 수명 주기: 세션 동안

핵심: Router Cache는 클라이언트에 있고 나머지 세 가지는 서버에 있습니다. revalidate는 주로 Data Cache와 Full Route Cache에 영향을 줍니다.
revalidatePath, revalidateTag, updateTag는 어떻게 다른가요?
revalidatePath(단일 페이지):
• 지정한 경로의 캐시를 무효화합니다.
• 용도: 사용자 작업 후 단일 페이지 갱신
• 사용: revalidatePath('/blog/post-1')

revalidateTag(여러 페이지):
• 지정한 태그가 붙은 모든 캐시를 무효화합니다.
• 용도: 사용자 작업 후 여러 페이지 갱신
• 사용: fetch할 때 태그를 붙이고 갱신할 때 태그를 무효화합니다.

updateTag(즉시 무효화, Next.js 15):
• 태그를 즉시 만료 상태로 표시합니다.
• 용도: 즉시 무효화가 필요한 상황
• 사용: updateTag('tag')

선택 기준:
• 단일 페이지 → revalidatePath
• 여러 페이지 → revalidateTag
• 즉시 무효화 → updateTag(Next.js 15)

주의: revalidateTag와 updateTag는 fetch의 tags와 함께 사용해야 합니다.
revalidate를 설정했는데도 데이터가 갱신되지 않는 이유는 무엇인가요?
가능한 원인:

1. 개발 환경에서 테스트함
• 개발 환경의 캐시 동작은 정확하지 않습니다.
• 반드시 프로덕션 환경을 사용합니다: npm run build && npm start

2. Router Cache가 남아 있음
• 클라이언트 캐시가 아직 유지되고 있습니다.
• 강력 새로고침을 시도합니다(Ctrl+Shift+R).

3. revalidate 설정 오류
• revalidate 값을 확인합니다.
• 설정 위치를 확인합니다.

4. Next.js 15는 기본적으로 캐시하지 않음
• cache: 'force-cache'를 명시해야 합니다.
• fetch의 cache 설정을 확인합니다.

5. 데이터 원본 자체가 갱신되지 않음
• 데이터 원본이 실제로 갱신됐는지 확인합니다.
• API 응답 데이터를 확인합니다.

진단 순서:
1. 개발 환경인지 확인
2. Router Cache 확인(강력 새로고침)
3. Full Route Cache 확인(revalidatePath)
4. Data Cache 확인(revalidateTag)
5. 데이터 원본 확인
Next.js 14와 15의 캐시는 어떻게 다른가요?
주요 차이:

Next.js 14:
• fetch를 기본으로 캐시합니다(getStaticProps와 같은 동작).
• 비활성화하려면 cache: 'no-store'를 명시합니다.
• 동작: 암시적 캐시

Next.js 15:
• fetch를 기본적으로 캐시하지 않습니다.
• 활성화하려면 cache: 'force-cache'를 명시합니다.
• 동작: 명시적 설정

마이그레이션 권장 사항:
• 모든 fetch 호출을 확인합니다.
• cache 옵션을 명시합니다.
• 캐시 동작을 테스트합니다.

코드 예시:
```tsx
// Next.js 14(기본 캐시)
fetch(url) // 자동 캐시

// Next.js 15(기본적으로 캐시하지 않음)
fetch(url, { cache: 'force-cache' }) // 명시적 설정 필요
```

핵심: Next.js 15의 철학은 ‘암시적 동작보다 명시적 설정’입니다. 어떤 데이터를 캐시할지 개발자가 직접 판단해야 합니다. 호환성을 깨는 변경이므로 마이그레이션할 때 주의해야 합니다.
revalidatePath와 revalidateTag는 각각 언제 사용해야 하나요?
revalidatePath(단일 페이지):
• 용도: 사용자 작업 후 단일 페이지 갱신
• 예: 글을 편집한 뒤 글 상세 페이지 갱신
• 사용: revalidatePath('/blog/post-1')

revalidateTag(여러 페이지):
• 용도: 사용자 작업 후 여러 페이지 갱신
• 예: 새 글을 게시한 뒤 모든 목록 페이지 갱신
• 사용: fetch할 때 태그를 붙이고 갱신할 때 태그를 무효화합니다.

선택 기준:
• 한 페이지만 갱신 → revalidatePath
• 여러 페이지 갱신 → revalidateTag

코드 예시:
```tsx
// 단일 페이지
revalidatePath('/blog/post-1')

// 여러 페이지
fetch(url, { next: { tags: ['posts'] } })
revalidateTag('posts') // 'posts' 태그가 붙은 모든 캐시 무효화
```

핵심: revalidateTag는 fetch의 tags와 함께 사용해야 하며 더 유연합니다.
개발 환경과 프로덕션 환경의 캐시 동작은 같은가요?
다릅니다. 개발 환경의 캐시 동작은 정확하지 않습니다.

개발 환경:
• 캐시 동작이 안정적이지 않습니다.
• 캐시하지 않을 수 있습니다.
• 캐시 테스트에 적합하지 않습니다.

프로덕션 환경:
• 캐시가 예상대로 동작합니다.
• 정상적으로 캐시합니다.
• 캐시 테스트에 적합합니다.

캐시 테스트:
• 반드시 프로덕션 환경을 사용합니다: npm run build && npm start
• 개발 환경에서 캐시를 테스트하지 않습니다.

자주 하는 실수:
• 개발 환경에서 캐시 테스트 → 결과가 정확하지 않음
• revalidate가 동작하지 않는다고 판단 → 실제로는 환경 문제

권장 사항: 캐시 동작은 항상 프로덕션 환경에서 테스트합니다.

6분 읽기 · 게시일: 2025년 12월 19일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog