Caching and Revalidating

김동현·2026년 3월 4일

캐싱(Caching)은 데이터 페칭(Data fetching) 결과나 기타 무거운 연산의 결과를 미리 저장해 두는 기술이에요. 이렇게 해두면 나중에 똑같은 데이터에 대한 요청이 들어왔을 때, 다시 무거운 작업을 할 필요 없이 훨씬 빠르게 응답을 제공할 수 있죠. 반면에 재검증(Revalidation)은 여러분의 애플리케이션 전체를 다시 빌드하지 않고도 캐시된 항목들을 업데이트할 수 있게 해주는 기능이랍니다.

👨‍🏫 강사의 보충 설명: > 쉽게 말해서 캐싱은 "한 번 한 숙제는 복사해두고 다음에 또 써먹기"이고, 재검증은 "숙제 내용이 바뀌었는지 확인하고 새 내용으로 갈아 끼우기"라고 생각하시면 돼요. 실무에서 서버의 부하를 줄이고 사용자에게 번개처럼 빠른 페이지를 보여주기 위해 프론트엔드 개발자가 반드시 마스터해야 하는 기술입니다!

Next.js는 캐싱과 재검증을 다루기 위한 몇 가지 API를 제공합니다. 이 가이드에서는 각 API를 언제, 어떻게 사용해야 하는지 하나씩 살펴볼게요.

fetch

기본적으로 fetch 요청들은 캐시되지 않아요. 만약 개별 요청을 캐시하고 싶다면 cache 옵션을 'force-cache'로 설정해야 합니다.

export default async function Page() {
  const data = await fetch('https://...', { cache: 'force-cache' })
}
export default async function Page() {
  const data = await fetch('https://...', { cache: 'force-cache' })
}

💡 알아두면 좋은 점 (Good to know): > 비록 fetch 요청이 기본적으로 캐시되지는 않지만, Next.js는 fetch 요청이 포함된 라우트를 사전 렌더링(pre-render)하고 그 HTML 결과물을 캐시한답니다. 만약 라우트가 무조건 동적(dynamic)으로 렌더링되도록 보장하고 싶다면, connection API를 사용하세요.

👨‍🏫 강사의 실무 팁: > Next.js 14 버전까지는 fetch가 기본적으로 캐시되는(force-cache가 디폴트) 정책이어서 많은 개발자들이 헷갈려 했었어요. 하지만 이제는 기본적으로 캐시하지 않는 것으로 변경되었기 때문에, 캐싱이 필요하다면 꼭 명시적으로 적어주어야 한다는 점을 기억하세요!

fetch 요청으로 반환된 데이터를 재검증(Revalidate)하려면, next.revalidate 옵션을 사용할 수 있어요.

export default async function Page() {
  const data = await fetch('https://...', { next: { revalidate: 3600 } })
}
export default async function Page() {
  const data = await fetch('https://...', { next: { revalidate: 3600 } })
}

이렇게 설정하면 지정된 초(seconds)가 지난 후에 데이터가 재검증됩니다. (위 예시에서는 3600초, 즉 1시간마다 최신 데이터를 가져오게 되겠네요!)

또한, 필요할 때 즉각적으로 캐시를 무효화(on-demand cache invalidation)하기 위해 fetch 요청에 태그(tag)를 달 수도 있어요.

export async function getUserById(id: string) {
  const data = await fetch(`https://...`, {
    next: {
      tags: ['user'],
    },
  })
}
export async function getUserById(id) {
  const data = await fetch(`https://...`, {
    next: {
      tags: ['user'],
    },
  })
}

자세한 내용은 fetch API 레퍼런스에서 확인해 보세요.

cacheTag

cacheTag캐시 컴포넌트(Cache Components) 내부에 있는 캐시된 데이터에 태그를 달아줘서, 원할 때 즉각적으로 재검증할 수 있게 해주는 기능이에요. 예전에는 캐시 태그를 다는 것이 fetch 요청에만 제한되어 있었고, 다른 작업들을 캐시하려면 실험적인 기능이었던 unstable_cache API를 써야만 했죠.

이제 캐시 컴포넌트와 함께라면, use cache 지시어(directive)를 사용해 어떤 연산이든 캐시할 수 있고, cacheTag를 써서 거기에 태그를 붙일 수 있습니다. 데이터베이스 쿼리, 파일 시스템 작업, 그리고 기타 서버 사이드 작업에도 모두 작동해요!

import { cacheTag } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheTag('products')

  const products = await db.query('SELECT * FROM products')
  return products
}
import { cacheTag } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheTag('products')

  const products = await db.query('SELECT * FROM products')
  return products
}

한 번 태그를 달아두면, revalidateTagupdateTag를 사용해서 특정 상품(products)에 대한 캐시 항목을 무효화할 수 있습니다.

💡 알아두면 좋은 점 (Good to know): > cacheTag캐시 컴포넌트(Cache Components)use cache 지시어와 함께 사용됩니다. 이 기능 덕분에 단순히 fetch를 넘어서 캐싱과 재검증을 활용할 수 있는 범위가 훨씬 넓어졌어요.

👨‍🏫 강사의 팁: > 외부 API를 쓸 때는 fetch를 쓰지만, 서버에서 직접 DB(Prisma, Drizzle 등)에 붙을 때는 fetch를 안 쓰잖아요? 그럴 때 예전에는 캐싱하기가 꽤나 까다로웠는데, 이제는 함수 상단에 'use cache' 한 줄 적고 cacheTag로 이름표를 붙여주기만 하면 끝입니다! 개발 경험이 정말 좋아진 부분이에요.

자세한 내용은 cacheTag API 레퍼런스를 확인하세요.

revalidateTag

revalidateTag는 태그와 특정 이벤트에 기반하여 캐시 항목을 재검증할 때 사용됩니다. 이 함수는 이제 두 가지 동작 방식을 지원해요:

  • profile="max" 사용 시 (권장): stale-while-revalidate 의미론을 사용합니다. 즉, 백그라운드에서 최신 데이터를 가져오는 동안 사용자에게는 일단 기존의(stale) 콘텐츠를 먼저 보여줍니다.
  • 두 번째 인자 생략 시: 즉시 캐시를 만료시켜버리는 레거시(예전) 동작입니다 (비권장, deprecated).

캐시된 데이터에 태그를 달아둔 후 (next.tags를 넣은 fetch를 썼거나, cacheTag 함수를 썼거나 상관없이), 라우트 핸들러(Route Handler)나 서버 액션(Server Action) 안에서 revalidateTag를 호출할 수 있습니다.

import { revalidateTag } from 'next/cache'

export async function updateUser(id: string) {
  // 데이터 변형(Mutate) 작업
  revalidateTag('user', 'max') // 권장 방식: stale-while-revalidate를 사용합니다
}
import { revalidateTag } from 'next/cache'

export async function updateUser(id) {
  // 데이터 변형(Mutate) 작업
  revalidateTag('user', 'max') // 권장 방식: stale-while-revalidate를 사용합니다
}

여러 함수에서 같은 태그를 재사용하여 한꺼번에 모두 재검증되도록 만들 수도 있어요.

👨‍🏫 강사의 보충 설명:
여기서 stale-while-revalidate 패턴이 프론트엔드 성능 최적화의 핵심입니다. 사용자가 화면을 요청했을 때, 서버가 새 데이터를 받아오느라 로딩 화면(Spinner)을 띄우고 기다리게 하는 게 아니라, 이전 데이터를 0.1초 만에 띄워주고 몰래 뒤에서 데이터를 업데이트하는 기법이에요. UX(사용자 경험) 향상을 위해 반드시 기억해 두세요!

자세한 내용은 revalidateTag API 레퍼런스에서 확인하세요.

updateTag

updateTag는 서버 액션(Server Actions)에서 "자신이 작성한 데이터를 즉시 읽어야 하는(read-your-own-writes)" 시나리오를 위해, 캐시된 데이터를 즉각적으로 만료시키도록 특별히 설계된 기능입니다. revalidateTag와 다르게, 이 함수는 오직 서버 액션 내부에서만 사용할 수 있으며 캐시 항목을 즉시 만료시킵니다.

import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  // 데이터베이스에 게시글 생성
  const post = await db.post.create({
    data: {
      title: formData.get('title'),
      content: formData.get('content'),
    },
  })

  // 새로운 게시글이 바로 보이도록 즉시 캐시를 만료시킵니다.
  updateTag('posts')
  updateTag(`post-${post.id}`)

  redirect(`/posts/${post.id}`)
}
import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData) {
  // 데이터베이스에 게시글 생성
  const post = await db.post.create({
    data: {
      title: formData.get('title'),
      content: formData.get('content'),
    },
  })

  // 새로운 게시글이 바로 보이도록 즉시 캐시를 만료시킵니다.
  updateTag('posts')
  updateTag(`post-${post.id}`)

  redirect(`/posts/${post.id}`)
}

revalidateTagupdateTag의 핵심적인 차이는 다음과 같습니다:

  • updateTag: 서버 액션에서만 사용 가능하며, "자신이 쓴 글 즉시 보기" 시나리오를 위해 캐시를 즉각 만료시킴.
  • revalidateTag: 서버 액션과 라우트 핸들러 모두에서 사용 가능하며, profile="max"를 통해 stale-while-revalidate 방식을 지원함.

👨‍🏫 강사의 실무 팁:
사용자가 댓글을 남겼는데, 화면에 내 댓글이 바로 안 보이고 예전 데이터만 보인다면? 당황해서 '등록' 버튼을 여러 번 누르겠죠? 이럴 땐 백그라운드 업데이트를 기다릴 여유가 없어요. 즉시 기존 캐시를 날려버리고 새로 그려야 합니다. 바로 이럴 때 updateTag를 써야 한다는 거 잊지 마세요!

자세한 내용은 updateTag API 레퍼런스를 확인하세요.

revalidatePath

revalidatePath는 특정 라우트(경로) 전체를 이벤트 발생 후 재검증할 때 사용합니다. 라우트 핸들러나 서버 액션 안에서 호출해서 사용할 수 있어요.

import { revalidatePath } from 'next/cache'

export async function updateUser(id: string) {
  // 데이터 변형 작업
  revalidatePath('/profile')
import { revalidatePath } from 'next/cache'

export async function updateUser(id) {
  // 데이터 변형 작업
  revalidatePath('/profile')

👨‍🏫 강사의 팁:
revalidatePath는 특정 URL 경로(.ex: /profile)의 렌더링 결과 자체를 무효화합니다. 아주 직관적이고 편하지만, 태그 기반 무효화(revalidateTag, updateTag)보다는 범위가 훨씬 커요. 섬세하게 캐시를 관리하고 성능을 쥐어짜야 하는 곳에서는 태그 방식을, 설정 페이지처럼 가끔 바뀌면서 한 번에 갈아엎어도 되는 곳에서는 revalidatePath를 적절히 섞어 쓰시면 좋습니다!

자세한 내용은 revalidatePath API 레퍼런스를 확인하세요.

unstable_cache

💡 알아두면 좋은 점 (Good to know): > unstable_cache는 실험적인 API였어요. 이제는 캐시 컴포넌트(Cache Components) 기능을 채택하여 unstable_cache 대신 use cache 지시어를 사용하는 것을 강력히 권장합니다. 자세한 사항은 캐시 컴포넌트 문서를 참고해 주세요.

unstable_cache는 데이터베이스 쿼리 결과나 기타 비동기 함수의 결과를 캐시할 수 있게 해주는 함수입니다. 사용하려면 캐시하고 싶은 함수를 unstable_cache로 감싸주면 돼요. 아래 예시를 볼까요?

import { db } from '@/lib/db'
export async function getUserById(id: string) {
  return db
    .select()
    .from(users)
    .where(eq(users.id, id))
    .then((res) => res[0])
}
import { db } from '@/lib/db'

export async function getUserById(id) {
  return db
    .select()
    .from(users)
    .where(eq(users.id, id))
    .then((res) => res[0])
}
import { unstable_cache } from 'next/cache'
import { getUserById } from '@/app/lib/data'

export default async function Page({
  params,
}: {
  params: Promise<{ userId: string }>
}) {
  const { userId } = await params

  const getCachedUser = unstable_cache(
    async () => {
      return getUserById(userId)
    },
    [userId] // 캐시 키(cache key)에 사용자 ID를 추가합니다.
  )
}
import { unstable_cache } from 'next/cache'
import { getUserById } from '@/app/lib/data'

export default async function Page({ params }) {
  const { userId } = await params

  const getCachedUser = unstable_cache(
    async () => {
      return getUserById(userId)
    },
    [userId] // 캐시 키(cache key)에 사용자 ID를 추가합니다.
  )
}

이 함수는 세 번째 선택적 인자로 객체를 받아들여 캐시가 어떻게 재검증되어야 하는지 정의할 수 있습니다. 다음과 같은 속성들을 지원해요:

  • tags: Next.js가 캐시를 재검증할 때 사용할 태그들의 배열입니다.
  • revalidate: 캐시가 재검증되어야 하는 시간(초 단위)입니다.
const getCachedUser = unstable_cache(
  async () => {
    return getUserById(userId)
  },
  [userId],
  {
    tags: ['user'],
    revalidate: 3600,
  }
)
const getCachedUser = unstable_cache(
  async () => {
    return getUserById(userId)
  },
  [userId],
  {
    tags: ['user'],
    revalidate: 3600,
  }
)

👨‍🏫 강사의 한마디: > 이전 버전(Next.js 14)까지는 DB 쿼리나 직접 짠 비동기 함수를 캐싱하려면 어쩔 수 없이 저 길고 복잡한 unstable_cache를 써야 했어요. 하지만 이제는 위에서 배운 'use cache' 덕분에 코드가 훨씬 깔끔해졌죠? 레거시 코드베이스를 다룰 때는 알아야 하니 눈에만 익혀두시고, 새 프로젝트에서는 가급적 'use cache'를 사용하도록 합시다!

자세한 내용은 unstable_cache API 레퍼런스를 확인하세요.

API 레퍼런스 (API Reference)

이 문서에서 언급된 기능들에 대해 더 깊이 공부하고 싶다면 아래의 API 레퍼런스를 읽어보세요.

  • fetch
    • 확장된 fetch 함수에 대한 API 레퍼런스입니다.
  • cacheTag
    • Next.js 애플리케이션에서 캐시 무효화를 관리하기 위해 cacheTag 함수를 어떻게 사용하는지 배워보세요.
  • revalidateTag
    • revalidateTag 함수에 대한 API 레퍼런스입니다.
  • updateTag
    • updateTag 함수에 대한 API 레퍼런스입니다.
  • revalidatePath
    • revalidatePath 함수에 대한 API 레퍼런스입니다.
  • unstable_cache
    • unstable_cache 함수에 대한 API 레퍼런스입니다.

모든 문서의 의미론적(semantic) 개요를 보려면 /docs/sitemap.md를 참조하세요.

사용 가능한 전체 문서의 색인(index)을 보려면 /docs/llms.txt를 참조하세요.


자, 여기까지 Next.js의 캐싱과 재검증에 대해 모두 번역하고 살펴보았습니다. 이 외에도 Next.js의 다른 파트에 대해 더 알고 싶거나, 코드를 직접 작성하다 막히는 부분이 있다면 언제든지 저에게 물어보세요! 더 도와드릴 부분이 있을까요?

profile
프론트에_가까운_풀스택_개발자

0개의 댓글