지난 세션에서 느린 데이터를 기다리는 동안에도 사용자에게 화면을 보여주는 방법을 배웠습니다. 이번에는 한 걸음 더 나아가, 같은 데이터를 매번 새로 가져와야 하는지 생각해 보겠습니다.
같은 블로그 글 목록을 100명이 동시에 요청한다고 생각해 보세요. 매번 데이터베이스에 쿼리를 보내거나 외부 API를 호출하면, 서버 부하와 비용이 불필요하게 늘어납니다. 글 목록이 1분 전과 달라지지 않았는데도 말이죠.
캐싱은 한 번 가져온 결과를 일정 기간 재사용하는 것입니다. 핵심 질문은 하나입니다: "얼마나 오래 재사용해도 괜찮은가?"
Next.js에서 fetch를 사용할 때 캐싱 동작을 제어할 수 있습니다. 기본값은 캐싱 없음 - 매 요청마다 새로 가져옵니다.
// 1. 기본값: 매번 새로 요청 (캐싱 없음)
const res = await fetch('http://localhost:4000/posts');
// 2. 한 번 가져오면 계속 재사용
const res = await fetch('http://localhost:4000/posts', {
cache: 'force-cache',
});
// 3. 60초마다 갱신
const res = await fetch('http://localhost:4000/posts', {
next: { revalidate: 60 },
});
| 옵션 | 동작 | 적합한 상황 |
|---|---|---|
| 기본값 (옵션 없음) | 매번 새로 요청 | 실시간 데이터 |
cache: 'force-cache' | 한 번 가져오면 재배포를 하기 전까지 재사용 | 거의 안 바뀌는 데이터 |
next: { revalidate: 초 } | 지정 시간 후 백그라운드 갱신 | 주기적으로 바뀌는 데이터 |
next: { revalidate: 60 }을 설정하면, 60초 동안은 캐싱된 결과를 사용하고 60초가 지난 뒤 다음 요청이 들어오면 백그라운드에서 데이터를 갱신합니다.
// app/blog/page.tsx
import { SearchablePostList } from '../components/SearchablePostList';
export default async function Blog() {
const res = await fetch('http://localhost:4000/posts', {
next: { revalidate: 60 }, // 60초마다 갱신
});
const posts = await res.json();
return (
<div>
<h1>글 목록</h1>
<SearchablePostList posts={posts} />
</div>
);
}
동작 흐름을 정리하면:
0초 → 첫 요청: 서버에서 fetch → 결과 캐싱 → HTML 전송
30초 → 요청: 캐싱된 결과 즉시 반환 (fetch 없음)
61초 → 요청: 캐싱된 결과 반환 + 백그라운드에서 새 데이터 fetch
62초 → 요청: 새로 가져온 데이터로 응답
사용자는 항상 빠른 응답을 받습니다. 데이터가 오래되었더라도 일단 캐싱된 결과를 먼저 보여주고, 새 데이터는 백그라운드에서 준비합니다. 이 패턴은 "오래된 데이터를 먼저 보여주면서 갱신한다"는 뜻에서 stale-while-revalidate라 불립니다.
시간 기반 재검증은 "60초마다 갱신"처럼 일정한 주기를 설정합니다. 하지만 글을 새로 작성했을 때 최대 60초를 기다려야 목록에 나타난다면 불편하겠죠.
revalidatePath를 사용하면 특정 경로의 캐시를 즉시 무효화할 수 있습니다:
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await fetch('http://localhost:4000/posts', {
method: 'POST',
body: JSON.stringify({ title }),
headers: { 'Content-Type': 'application/json' },
});
revalidatePath('/blog'); // /blog 경로의 캐시 즉시 무효화
}
위 코드는 다음 세션에서 배울 Server Action입니다. 핵심만 보면, 데이터를 변경한 뒤 revalidatePath('/blog')를 호출해서 해당 경로의 캐시를 무효화합니다. 다음 요청부터 새 데이터를 가져옵니다.
revalidatePath는 특정 경로의 캐시를 무효화합니다. revalidateTag는 태그를 기준으로 여러 fetch 캐시를 한꺼번에 무효화할 수 있지만, 이 튜토리얼에서는 더 직관적인 revalidatePath를 사용합니다.
지금까지 fetch 옵션으로 캐싱을 제어하는 방법을 배웠습니다. 이 방식은 개별 HTTP 요청의 응답을 캐싱합니다. Next.js 16의 "use cache" 지시어는 더 넓은 단위로 캐싱합니다 - 함수나 컴포넌트의 실행 결과 자체를 캐싱하므로, 여러 fetch 호출과 데이터 가공 로직을 포함한 전체 결과를 한 번에 캐싱할 수 있습니다:
"use cache";
export async function getPosts() {
const res = await fetch('http://localhost:4000/posts');
return res.json();
}
'use client', 'use server'와 같은 패턴입니다. 데이터 요청(fetch), 데이터베이스 조회, 또는 계산 비용이 큰 작업의 결과를 재사용하기 위해 사용됩니다. 이 튜토리얼에서는 fetch 옵션 기반 캐싱을 중심으로 설명합니다. "use cache"는 더 큰 프로젝트에서 유용하게 사용할 수 있는 방식입니다.
"use cache"를 사용하려면 next.config.ts에서 cacheComponents: true 설정이 필요합니다. 이 플래그는 이전의 dynamicIO, useCache 등을 하나로 통합한 것입니다.
| 개념 | 핵심 내용 |
|---|---|
| 기본값 | Next.js 16에서 fetch는 기본적으로 캐싱하지 않음 |
force-cache | 캐시된 응답을 재사용. 거의 안 바뀌는 데이터에 적합 |
| 시간 기반 재검증 | next: { revalidate: 초 }로 주기적 갱신 |
| 요청 기반 재검증 | revalidatePath로 데이터 변경 시 즉시 무효화 |
"use cache" | Next.js 16의 새 지시어. 페이지/컴포넌트/함수 단위 캐싱 |
다음 세션에서는 지금까지 빠져 있던 마지막 퍼즐을 채웁니다. 데이터를 읽기만 했는데, 글을 추가하거나 수정하려면 어떻게 해야 할까요? Server Actions를 배웁니다.