Data Cache

작성일: 2025. 10. 11최종 수정: 2025. 10. 11. 02시 02분

데이터 캐시란

App Router의 데이터 캐시는 서버에서 가져온 데이터를 저장하고 재사용하는 메커니즘이다. 동일한 데이터를 반복해서 외부 API나 데이터베이스에서 가져오는 대신, 한 번 가져온 데이터를 서버 메모리에 저장해두고 다음 요청에서 빠르게 응답할 수 있다.

데이터 캐시를 사용하려면 fetch API를 사용해야 한다. Next.js는 Web API의 표준 fetch를 확장하여 다양한 캐싱 옵션을 제공한다.

fetch API와 캐싱 옵션

Next.js의 fetch는 cachenext 옵션을 통해 캐싱 동작을 제어할 수 있다.

javascript
// 기본 사용법
const res = await fetch('<https://api.example.com/data>', {
  cache: 'force-cache',  // 캐싱 전략
  next: {
    revalidate: 60,  // 재검증 주기
    tags: ['posts']  // 캐시 태그
  }
});

각 옵션은 데이터의 특성과 업데이트 빈도에 따라 선택한다.

no-store - 캐싱 비활성화

데이터 페칭 결과를 저장하지 않는다. 매 요청마다 서버에서 새로운 데이터를 가져온다.

javascript
async function RealTimePrice() {
  const res = await fetch('<https://api.example.com/stock-price>', {
    cache: 'no-store'
  });
  const data = await res.json();

  return <div>현재 가격: {data.price}원</div>;
}

Next.js 서버가 브라우저로부터 접속 요청을 받으면 사전 렌더링을 진행하게 되는데, no-store로 설정된 fetch는 캐시를 무시하고 바로 원본 서버로 요청을 보낸 뒤 브라우저에 응답한다.

Next.js 15부터는 data cache의 기본값이 no-store로 변경되었다. 이전 버전에서는 force-cache가 기본값이었지만, 최신 데이터를 제공하는 것이 더 중요하다는 판단에 따라 기본 동작이 바뀌었다.

사용 시나리오

  • 실시간 주식 가격, 환율 정보
  • 사용자별로 다른 데이터 (개인화된 콘텐츠)
  • 자주 변경되는 데이터 (뉴스, 댓글)

force-cache - 영구 캐싱

요청의 결과를 무조건 캐싱한다. 한 번 호출된 이후에는 재배포하거나 수동으로 캐시를 초기화하기 전까지 다시 호출되지 않는다.

javascript
async function StaticContent() {
  const res = await fetch('<https://api.example.com/terms>', {
    cache: 'force-cache'
  });
  const data = await res.json();

  return <div>{data.content}</div>;
}

첫 번째 요청에서는 cache miss가 발생하지만, 서버에서 데이터를 가져온 후 캐시에 저장(set)한다. 두 번째 요청부터는 cache hit이 발생하여 캐시된 데이터를 즉시 반환한다.

plain
1번째 요청: Miss → 서버 요청 → Set (캐시 저장)
2번째 요청: Hit → 캐시된 데이터 반환
3번째 요청: Hit → 캐시된 데이터 반환

사용 시나리오

  • 거의 변경되지 않는 데이터 (이용약관, 회사 소개)
  • 정적 콘텐츠 (블로그 글, 제품 설명)
  • 외부 API 호출 비용이 높은 경우

revalidate - 주기적 재검증

특정 시간을 주기로 캐시를 업데이트한다. [[사전 렌더링 방식 - SSR, SSG, ISR|Page Router의 ISR 방식]]과 유사하게 동작한다.

javascript
async function ProductList() {
  const res = await fetch('<https://api.example.com/products>', {
    next: { revalidate: 3600 }  // 1시간마다 재검증
  });
  const products = await res.json();

  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

동작 과정은 다음과 같다.

plain
0초: 첫 요청 → 서버 요청 → 캐시 저장 (fresh)
30초: 두 번째 요청 → 캐시 반환 (fresh)
3600초: 캐시가 stale로 변경
3601초: 요청 → 캐시 반환 (stale) + 백그라운드에서 재검증 시작
3605초: 재검증 완료 → 캐시 업데이트 (fresh)
3610초: 요청 → 새로운 캐시 반환 (fresh)

정해진 주기(revalidate 시간)가 지나면 캐시가 stale 상태가 된다. 이 상태에서 요청이 들어오면 일단 오래된 캐시를 반환하고, 백그라운드에서 데이터를 새로 가져온다. 다음 요청부터는 업데이트된 데이터를 받을 수 있다.

사용 시나리오

  • 자주는 아니지만 가끔 업데이트되는 데이터 (제품 목록, 카테고리)
  • 실시간성이 중요하지 않은 데이터 (통계, 인기 게시물)
  • 외부 API 호출을 줄이면서도 적절한 최신성을 유지하고 싶을 때

tags - 온디맨드 재검증

캐시에 태그를 붙여서 원하는 시점에 특정 태그의 캐시를 업데이트할 수 있다.

javascript
// 데이터 페칭
async function BlogPost({ id }) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    next: { tags: ['posts', `post-${id}`] }
  });
  const post = await res.json();

  return <article>{post.content}</article>;
}

위 코드에서 fetch 요청에 tags: ['posts', 'post-${id}']를 지정했다. 이제 다른 곳에서 캐시를 수동으로 무효화할 수 있다.

javascript
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache';

export async function POST(request) {
  const { tag } = await request.json();

  revalidateTag(tag);  // 특정 태그의 캐시 무효화

  return Response.json({ revalidated: true });
}

revalidateTag('posts')를 호출하면 'posts' 태그가 붙은 모든 캐시가 즉시 무효화되고, 다음 요청에서 새로운 데이터를 가져온다.

사용 시나리오

  • CMS에서 콘텐츠 수정 후 즉시 반영
  • 관리자가 제품 정보를 업데이트한 경우
  • 사용자가 댓글을 작성한 직후
  • Webhook을 통해 외부 시스템 변경 감지

revalidatePath와의 차이

javascript
import { revalidatePath } from 'next/cache';

// 특정 경로의 캐시 무효화
revalidatePath('/blog/post-1');

// 특정 태그의 캐시 무효화 (더 세밀한 제어)
revalidateTag('post-1');

revalidatePath는 페이지 단위로 캐시를 무효화하는 반면, revalidateTag는 fetch 요청 단위로 더 세밀하게 제어할 수 있다. 하나의 페이지에 여러 fetch 요청이 있을 때 특정 요청만 선택적으로 무효화하려면 태그를 사용한다.

실전 활용 패턴

페이지별로 다른 전략 사용

javascript
// 홈페이지 - 자주 변경되는 데이터
async function HomePage() {
  const news = await fetch('/api/news', { cache: 'no-store' });
  const popular = await fetch('/api/popular', { next: { revalidate: 300 } });

  return (
    <div>
      <NewsList data={news} />
      <PopularPosts data={popular} />
    </div>
  );
}

하나의 페이지에서도 데이터의 특성에 따라 다른 캐싱 전략을 조합할 수 있다. 실시간 뉴스는 no-store로 매번 새로 가져오고, 인기 게시물은 5분마다 업데이트한다.

캐시 전략 선택 가이드

plain
데이터 변경 빈도 및 중요도에 따른 선택

실시간 필수 → no-store
거의 변경 안됨 → force-cache
주기적 업데이트 → revalidate (시간 지정)
이벤트 기반 업데이트 → tags + revalidateTag

캐싱은 성능과 최신성 사이의 트레이드오프다. 데이터의 특성과 사용자 경험을 고려하여 적절한 전략을 선택해야 한다.