frontend

Next.js App Router fetch 캐싱과 revalidate 완벽 가이드 — 언제 데이터가 갱신되나

Next.js App Router에서 fetch는 기본으로 캐싱된다. 언제 캐시를 쓰고 언제 갱신할지 제어하는 cache, next.revalidate, next.tags 옵션과 revalidatePath, revalidateTag를 정리했다.

Next.jsApp Routerfetch캐싱revalidate
Next.js App Router 서버 컴포넌트에서 fetch 요청에 next.revalidate와 cache 옵션을 설정하는 코드 예시
  • ·Next.js App Router fetch 기본값: 요청 메모이제이션(동일 요청 중복 제거), Data Cache(빌드 후 캐시)
  • ·next.revalidate: 초 단위 재검증 주기, 0이면 매 요청마다 새로 가져옴
  • ·revalidatePath: 특정 경로의 캐시를 즉시 무효화하는 서버 함수
  • ·unstable_cache: fetch 외 데이터 소스(DB 쿼리 등)를 Next.js Data Cache에 포함시키는 방법
Next.js App Router로 블로그를 만들면서 글을 수정해도 페이지에 반영이 안 되는 문제가 있었다. fetch에 cache: 'force-cache'가 기본값이라는 걸 몰랐고, revalidate 없이 빌드된 페이지는 재배포 전까지 업데이트되지 않는다는 걸 직접 겪고 나서야 이해했다. revalidatePath를 Server Action에서 호출하도록 바꾼 뒤부터는 글을 수정하면 해당 페이지 캐시가 즉시 무효화됐다.

Next.js App Router fetch 캐싱 동작 이해하기

Next.js App Router에서 fetch 캐싱이 기본으로 동작하는 방식

Next.js App Router에서 서버 컴포넌트의 fetch 요청은 기본적으로 두 단계의 캐싱을 거친다. 첫 번째는 요청 메모이제이션(Request Memoization)이다. 같은 렌더링 패스 안에서 동일한 URL과 옵션으로 여러 컴포넌트가 fetch를 호출하면 실제 네트워크 요청은 한 번만 발생하고 나머지는 메모이제이션된 결과를 받는다. 이 메모이제이션은 단일 요청 생명주기 내에서만 유효하다. 두 번째는 Data Cache다. 첫 요청의 응답이 Next.js의 서버 사이드 캐시에 저장된다. 기본값(cache: 'force-cache')으로 설정하면 빌드 시 생성된 캐시가 재배포 전까지 유지된다. 이 동작이 예상치 못한 결과를 낳는 경우가 많다. API 데이터가 바뀌어도 Next.js가 캐시된 응답을 계속 반환하기 때문에 화면에 반영되지 않는다. Pages Router에서 getStaticProps와 getServerSideProps의 차이와 비슷한 개념이지만, App Router에서는 fetch 옵션으로 컴포넌트 단위로 세밀하게 제어할 수 있다. 데이터가 얼마나 자주 바뀌는지에 따라 적절한 캐시 전략을 선택하는 것이 App Router 성능 최적화의 핵심이다.

Next.js fetch 캐시 옵션 — cache와 next.revalidate의 차이

Next.js App Router에서 fetch 캐싱을 제어하는 옵션은 크게 두 가지다. cache 옵션은 Web Fetch API 표준과 호환된다. cache: 'force-cache'는 기본값으로 캐시가 있으면 캐시를 쓰고 없으면 네트워크 요청 후 캐시에 저장한다. cache: 'no-store'는 캐시를 전혀 쓰지 않고 매 요청마다 새로 가져온다. 이 옵션을 쓴 컴포넌트가 있는 페이지는 동적 렌더링으로 전환된다. next.revalidate는 Next.js 전용 확장 옵션이다. next: { revalidate: 60 }으로 설정하면 마지막 요청 후 60초가 지난 다음 요청부터 백그라운드에서 데이터를 새로 가져온다. 이 방식을 ISR(Incremental Static Regeneration)이라고 한다. 사용자는 항상 캐시된 응답을 즉시 받지만 주기적으로 데이터가 갱신된다. next: { revalidate: 0 }은 cache: 'no-store'와 동일하게 동작한다. 뉴스 피드처럼 자주 바뀌는 데이터는 revalidate를 짧게, 공식 문서나 정책 페이지처럼 거의 안 바뀌는 데이터는 revalidate를 길게 또는 force-cache로 설정하는 게 좋다.

Next.js revalidate와 캐시 무효화 설정하기

Next.js App Router에서 fetch revalidate를 설정하는 방법

서버 컴포넌트에서 fetch를 호출할 때 두 번째 인수에 옵션 객체를 전달한다. 자주 갱신이 필요한 데이터라면 { next: { revalidate: 60 } }처럼 초 단위로 재검증 주기를 설정한다. 거의 변하지 않는 정적 데이터라면 { cache: 'force-cache' }를 쓰거나 옵션을 생략한다. 매 요청마다 최신 데이터가 필요한 경우에는 { cache: 'no-store' }를 쓴다. 페이지 단위로 revalidate를 설정할 수도 있다. 페이지의 page.tsx나 layout.tsx에서 export const revalidate = 60처럼 숫자를 export하면 해당 페이지의 모든 fetch에 기본 revalidate가 적용된다. 컴포넌트 레벨의 fetch 옵션이 페이지 레벨 설정보다 우선된다. export const dynamic = 'force-dynamic'을 설정하면 페이지 전체가 동적 렌더링으로 전환되어 항상 최신 데이터를 가져온다. 처음 App Router를 쓸 때 기본 캐싱 때문에 데이터가 갱신이 안 되는 문제를 겪었다면, 우선 fetch에 { cache: 'no-store' }를 추가해서 캐싱을 끄고 동작을 확인한 다음 적절한 revalidate 전략으로 조정하는 방식이 디버깅하기 수월하다.

// 60초마다 재검증
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 },
})

// 캐시 없이 항상 최신
const data = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
})

// 페이지 단위 설정 (page.tsx)
export const revalidate = 3600 // 1시간
export const dynamic = 'force-dynamic' // 항상 동적

Next.js revalidatePath와 revalidateTag로 캐시를 즉시 무효화하는 방법

시간 기반 revalidate는 데이터가 언제 바뀌는지 예측할 수 있는 경우에 적합하다. CMS에서 글을 수정하거나 사용자가 데이터를 변경했을 때처럼 특정 이벤트 발생 시 즉시 캐시를 무효화해야 한다면 on-demand revalidation을 써야 한다. Next.js는 revalidatePath와 revalidateTag 두 가지 함수를 제공한다. revalidatePath('/posts')를 호출하면 해당 경로와 하위 경로의 캐시가 즉시 무효화된다. 다음 요청 시 데이터를 새로 가져온다. Server Action이나 Route Handler에서 호출할 수 있다. revalidateTag를 쓰려면 fetch에 next: { tags: ['posts'] }처럼 태그를 설정해두고, 무효화할 때 revalidateTag('posts')를 호출한다. 같은 태그를 가진 모든 fetch 결과가 한 번에 무효화된다. 블로그에서 글을 수정하는 Server Action 안에 revalidatePath('/posts/[slug]')를 추가했더니 수정 직후 해당 페이지 캐시가 즉시 초기화되어 새 내용이 반영됐다. 이 패턴이 App Router에서 CMS 연동이나 관리자 기능 구현 시 가장 많이 쓰인다.

Next.js 캐싱 전략 실용 패턴

Next.js App Router에서 데이터 특성별 fetch 캐시 전략을 선택하는 방법

모든 데이터에 동일한 캐시 전략을 적용하는 것은 비효율적이다. 데이터가 얼마나 자주 바뀌는지, 오래된 데이터를 보여줘도 괜찮은지에 따라 전략을 다르게 가져가야 한다. 변경이 거의 없는 설정 데이터나 공지사항은 force-cache 또는 매우 긴 revalidate로 설정한다. 재배포 없이는 바뀌지 않는 데이터다. 블로그 글, 제품 목록처럼 가끔 수정되는 데이터는 ISR 방식이 적합하다. revalidate를 수십 분에서 몇 시간으로 설정하거나, 수정 시 revalidatePath로 즉시 무효화하는 방식을 쓴다. 사용자별로 달라지는 데이터나 실시간으로 변하는 데이터는 no-store 또는 dynamic: 'force-dynamic'으로 설정한다. 장바구니, 알림, 대시보드 지표가 여기 해당한다. 같은 페이지 안에서도 헤더의 네비게이션 메뉴는 force-cache로, 피드 데이터는 revalidate: 60으로 컴포넌트마다 다른 전략을 쓸 수 있다. App Router의 이 유연성이 Pages Router에 비해 더 세밀한 성능 최적화를 가능하게 하는 핵심이다.

Next.js unstable_cache로 fetch 외 데이터 소스를 캐싱하는 방법

Next.js의 Data Cache는 기본적으로 fetch 요청에만 적용된다. Prisma나 Drizzle 같은 ORM으로 DB를 직접 쿼리하거나 파일 시스템을 읽는 경우는 자동으로 캐싱되지 않는다. unstable_cache 함수를 사용하면 fetch 외의 데이터 소스도 Next.js Data Cache에 포함시킬 수 있다. 캐싱할 데이터를 가져오는 함수를 unstable_cache로 감싸고, 캐시 키와 태그, revalidate 옵션을 설정한다. 이렇게 감싼 함수를 호출하면 Next.js가 캐시를 확인하고 없거나 만료됐을 때만 실제 함수를 실행한다. 태그를 설정해두면 revalidateTag로 DB 쿼리 결과도 on-demand로 무효화할 수 있다. unstable_이라는 접두사가 붙어 있어 불안해 보이지만, Next.js 공식 문서에서 사용을 권장하는 API이며 Next.js 15에서 안정화된 cache 함수로 대체됐다. React 19의 use cache 지시어가 이 기능을 더 직관적인 방식으로 제공한다. DB 쿼리 캐싱을 적용한 후 동일한 데이터를 여러 컴포넌트에서 쓸 때 DB 부하가 크게 줄어드는 걸 직접 확인했다.

자주 묻는 질문

데이터를 수정했는데 페이지에 반영이 안 됩니다. 어떻게 해야 하나요?+

fetch에 force-cache가 적용되어 있어서 캐시된 데이터를 계속 보여주는 경우입니다. 빠른 확인을 위해 fetch 옵션에 cache: 'no-store'를 추가해서 캐싱을 끄고 데이터가 바뀌는지 확인하세요. 이후 Server Action에서 revalidatePath나 revalidateTag로 필요할 때만 캐시를 무효화하는 방식으로 전환하면 됩니다.

revalidate를 0으로 설정하면 cache: 'no-store'와 같은가요?+

동작은 유사하지만 완전히 같지 않습니다. revalidate: 0은 매 요청마다 재검증하지만 캐시 항목 자체는 남겨둡니다. cache: 'no-store'는 캐시를 전혀 사용하지 않습니다. 일반적으로 항상 최신 데이터가 필요한 경우는 no-store를 권장합니다.

revalidatePath는 Server Action 외에 어디서 호출할 수 있나요?+

Route Handler(API 라우트)에서도 호출할 수 있습니다. Webhook을 받아서 특정 데이터가 변경됐을 때 Route Handler에서 revalidateTag를 호출하는 패턴으로 CMS와 Next.js를 연동할 때 많이 씁니다.

Next.js 개발 환경(npm run dev)에서는 캐싱이 동작하지 않는 것 같은데 정상인가요?+

정상입니다. next dev 모드에서는 매 요청마다 새로 fetch해서 개발 중 데이터 변경을 바로 확인할 수 있게 합니다. 실제 캐싱 동작은 next build 후 next start 환경에서 확인해야 합니다.

관련 글