App Router 데이터 캐시
데이터 캐시란
App Router의 데이터 캐시는 서버에서 가져온 데이터를 저장하고 재사용하는 메커니즘이다. 동일한 데이터를 반복해서 외부 API나 데이터베이스에서 가져오는 대신, 한 번 가져온 데이터를 서버 메모리에 저장해두고 다음 요청에서 빠르게 응답할 수 있다.
데이터 캐시를 사용하려면 fetch API를 사용해야 한다. Next.js는 Web API의 표준 fetch를 확장하여 다양한 캐싱 옵션을 제공한다.
fetch API와 캐싱 옵션
Next.js의 fetch는 cache와 next 옵션을 통해 캐싱 동작을 제어할 수 있다.
// 기본 사용법
const res = await fetch('<https://api.example.com/data>', {
cache: 'force-cache', // 캐싱 전략
next: {
revalidate: 60, // 재검증 주기
tags: ['posts'] // 캐시 태그
}
});
각 옵션은 데이터의 특성과 업데이트 빈도에 따라 선택한다.
no-store - 캐싱 비활성화
데이터 페칭 결과를 저장하지 않는다. 매 요청마다 서버에서 새로운 데이터를 가져온다.
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 - 영구 캐싱
요청의 결과를 무조건 캐싱한다. 한 번 호출된 이후에는 재배포하거나 수동으로 캐시를 초기화하기 전까지 다시 호출되지 않는다.
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이 발생하여 캐시된 데이터를 즉시 반환한다.
1번째 요청: Miss → 서버 요청 → Set (캐시 저장)
2번째 요청: Hit → 캐시된 데이터 반환
3번째 요청: Hit → 캐시된 데이터 반환
사용 시나리오
- 거의 변경되지 않는 데이터 (이용약관, 회사 소개)
- 정적 콘텐츠 (블로그 글, 제품 설명)
- 외부 API 호출 비용이 높은 경우
revalidate - 주기적 재검증
특정 시간을 주기로 캐시를 업데이트한다. [[사전 렌더링 방식 - SSR, SSG, ISR|Page Router의 ISR 방식]]과 유사하게 동작한다.
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>
);
}
동작 과정은 다음과 같다.
0초: 첫 요청 → 서버 요청 → 캐시 저장 (fresh)
30초: 두 번째 요청 → 캐시 반환 (fresh)
3600초: 캐시가 stale로 변경
3601초: 요청 → 캐시 반환 (stale) + 백그라운드에서 재검증 시작
3605초: 재검증 완료 → 캐시 업데이트 (fresh)
3610초: 요청 → 새로운 캐시 반환 (fresh)
정해진 주기(revalidate 시간)가 지나면 캐시가 stale 상태가 된다. 이 상태에서 요청이 들어오면 일단 오래된 캐시를 반환하고, 백그라운드에서 데이터를 새로 가져온다. 다음 요청부터는 업데이트된 데이터를 받을 수 있다.
사용 시나리오
- 자주는 아니지만 가끔 업데이트되는 데이터 (제품 목록, 카테고리)
- 실시간성이 중요하지 않은 데이터 (통계, 인기 게시물)
- 외부 API 호출을 줄이면서도 적절한 최신성을 유지하고 싶을 때
tags - 온디맨드 재검증
캐시에 태그를 붙여서 원하는 시점에 특정 태그의 캐시를 업데이트할 수 있다.
// 데이터 페칭
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}']를 지정했다. 이제 다른 곳에서 캐시를 수동으로 무효화할 수 있다.
// 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와의 차이
import { revalidatePath } from 'next/cache';
// 특정 경로의 캐시 무효화
revalidatePath('/blog/post-1');
// 특정 태그의 캐시 무효화 (더 세밀한 제어)
revalidateTag('post-1');
revalidatePath는 페이지 단위로 캐시를 무효화하는 반면, revalidateTag는 fetch 요청 단위로 더 세밀하게 제어할 수 있다. 하나의 페이지에 여러 fetch 요청이 있을 때 특정 요청만 선택적으로 무효화하려면 태그를 사용한다.
실전 활용 패턴
페이지별로 다른 전략 사용
// 홈페이지 - 자주 변경되는 데이터
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분마다 업데이트한다.
캐시 전략 선택 가이드
데이터 변경 빈도 및 중요도에 따른 선택
실시간 필수 → no-store
거의 변경 안됨 → force-cache
주기적 업데이트 → revalidate (시간 지정)
이벤트 기반 업데이트 → tags + revalidateTag
캐싱은 성능과 최신성 사이의 트레이드오프다. 데이터의 특성과 사용자 경험을 고려하여 적절한 전략을 선택해야 한다.