Next.js는 기본적으로 서버 렌더링과 캐싱 기능을 통해 성능 최적화를 지원하며, 데이터를 효율적으로 처리하기 위해 다양한 데이터 캐시 옵션을 제공한다. 특히, fetch 메서드에 확장된 캐시 옵션을 활용하면 불필요한 요청을 줄이고, 적시에 데이터를 갱신하며, 최적의 사용자 경험을 제공할 수 있다. 이번 글에서는 Next.js에서 제공하는 데이터 캐시 옵션과 실습 예제를 통해 이를 이해해 보도록 하자.
데이터 캐시는 fetch
메서드를 활용해 API 서버로부터 불러온 데이터를 Next.js 서버에 저장(캐싱)하는 기능이다. 이를 통해 다음과 같은 이점이 있다.
1. 불필요한 요청 방지
2. 성능 개선
3. 갱신 주기 설정
1. 캐시 옵션 설정
const response = await fetch('/api/data', { cache: 'force-cache' });
위와 같이 cache: "force-cache"
옵션을 설정하면, 해당 요청의 결과가 캐싱되며 이후에는 데이터를 다시 요청하지 않고 캐시된 데이터를 반환한다.
2. 다양한 캐시 옵션
fetch
는 다양한 캐싱 전략을 제공한다. 이를 활용해 실시간 데이터부터 정적 데이터까지 다양한 요구를 충족할 수 있다.3. axios와의 차이점
fetch
는 일반적인 fetch메서드가 아닌, 캐싱, 재검증, 태그 기반 갱신 등 Next.js에 특화된 기능이 포함된 확장판 메서드이기 때문이다.4. 데이터 캐시 활용의 장점
한 번 불러온 데이터를 Next.js 서버에서 저장해 불필요한 추가 요청을 방지.
특정 시간 간격으로 데이터를 갱신하거나, 요청 시 데이터를 최신화할 수 있는 다양한 전략 제공.
페이지 로딩 속도 향상 및 서버 부하 감소.
Next.js의 fetch 메서드는 일반적인 브라우저의 fetch와 다르다. Next.js에서 제공하는 확장판으로, 캐싱 옵션과 서버 최적화 기능이 포함되어 있다.
cache: "no-store"
데이터를 캐싱하지 않고, 항상 새롭게 요청한다.
실시간 데이터를 필요로 하는 경우에 사용. (ex: 최신 뉴스, 실시간 주식 가격)
cache: "no-store"
설정 시, 데이터는 요청할 때마다 항상 백엔드 API 서버로부터 불러온다.
Next.js 서버와 브라우저 간 캐싱이 발생하지 않는다.
async function AllBooks() {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`, {
cache: "no-store",
});
if (!response.ok) {
return <div>오류가 발생했습니다 ...</div>;
}
const allBooks = await response.json();
return (
<div>
{allBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
이렇게 fetch메서드의 캐싱 옵션을 "no-store"
라고 설정하게 되면 브라우저로부터 접속요청이 들어왔을 때, Next서버에서는 사전 렌더링을 진행하게 되고, 이 과정에서 cache옵션이 "no-store"
로서 캐싱된 데이터를 사용하지 않기 때문에 데이터 캐시는 skip
이 되고, 아무런 캐싱도 동작하지 않게 되어 페이지에 접속할 때마다 매번 새로운 데이터를 백엔드 서버에 요청해서 응답받은 데이터로 페이지를 생성해 브라우저에게 반환하게 된다.
cache: "force-cache"
요청 결과를 항상 캐싱하여, 다시 요청하지 않고 캐시된 데이터를 반환한다.
데이터가 자주 변경되지 않는 경우에 사용. (ex: 정적 콘텐츠, 소개 페이지)
첫 요청 후 데이터는 Next.js 서버에 저장된다.
이후 동일한 요청에서는 캐시된 데이터가 반환된다.
force-cache
는 브라우저의 새로고침이나 추가 요청 시에도 동일한 데이터를 반환한다.
async function RecoBooks() {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/random`, {
cache: "force-cache",
});
if (!response.ok) {
return <div>오류가 발생했습니다 ...</div>;
}
const recoBooks = await response.json();
return (
<div>
{recoBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
브라우저에서 접속 요청이 들어오면, 페이지 렌더링 과정에서 데이터 페칭이 발생한다.
이때 캐시에 저장된 데이터가 없으므로 MISS로 판정된다.
백엔드 서버로부터 데이터를 요청하고 가져와 이를 캐시에 SET(저장)
하고, 해당 데이터로 페이지를 생성해서 브라우저에 반환한다.
이후 동일한 요청이 들어오면, 캐시된 데이터를 활용하여 응답한다.
이때 데이터는 HIT(발견)
으로 판정되며, 백엔드 서버에 추가 요청 없이 캐시 데이터를 반환한다.
콘솔을 확인해 보면 cache hit
되어서 추가적인 요청이 발생하지 않았다는 사실도 알 수 있다.
그리고 이때 캐싱된 데이터는 json
형태로 Next서버 안에 보관이 된다. 이 데이터를 직접 확인해 보려면 .next
폴더 안에 cache
폴더 안에 fetch-cache
폴더 안에 들어있는 파일에서 확인할 수 있다.
{ next: { revalidate: N } }
Page Router의 ISR(Incremental Static Regeneration)과 유사한 기능을 제공한다.
특정 주기(N 초)로 캐시된 데이터를 자동으로 갱신한다.
자주 변경되는 데이터를 캐싱하되, 주기적으로 갱신이 필요한 경우에 사용. (ex: 상품 목록, 인기 게시글)
첫 요청 시 데이터를 캐시한다.
설정한 시간(ex: 3초) 이후에는 백그라운드에서 데이터를 갱신한다.
최신 데이터가 준비되기 전에 캐시된 데이터를 먼저 반환하므로 빠른 응답 속도를 제공한다.
async function RecoBooks() {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/random`, {
next: { revalidate: 3 }, // 3초마다 데이터 갱신
});
if (!response.ok) {
return <div>오류가 발생했습니다 ...</div>;
}
const recoBooks = await response.json();
return (
<div>
{recoBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
{ next: { revalidate: N } }
옵션 동작 원리브라우저에서 초기 요청이 들어오면 데이터 페칭이 실행되고, 캐싱된 데이터가 없으므로 MISS
로 판정된다.
백엔드 서버에서 데이터를 받아와 캐시(SET
)에 저장한 뒤, 이를 반환한다.
이후 요청에서는 데이터 캐시에서 HIT(발견)
된 값을 반환한다.
설정된 시간이 지나기 전까지는 캐시된 데이터를 그대로 사용한다.
설정된 시간이 지나면, 브라우저에 접속 요청이 들어왔을 때 데이터를 STALE(상하다)
상태로 설정을 해두고, 이 상한 데이터라도 먼저 응답을 해줘서 페이지를 생성하게 해서 빠른 응답 속도를 유지한다. 그리고 나서 백엔드에서 최신 데이터를 받아와 Revalidate(갱신) 작업이 수행된다.
갱신된 데이터는 캐시에 저장(SET
)되어 이후 요청에 사용된다.
이후의 요청에 대해서는 방급 업데이트된 최신 데이터를 캐시해서 다음 접속 요청시에는 빠르게 페이지를 생성해 봔환하게 된다.
{ next: { tags: ['tag-name'] } }
요청 시 특정 태그(tag-name
)를 기준으로 데이터를 온디맨드로 갱신한다.
관리자가 특정 데이터를 갱신할 때 사용 (ex: 블로그 게시글 업데이트, 상품 정보 수정)
Next.js의 On-Demand ISR과 유사한 동작을 한다.
갱신 요청이 들어올 때만 데이터를 새롭게 요청한다.
이 옵션은 별도의 API나 관리자 도구와 함께 사용해야 한다.
필요할 때만 데이터를 갱신하므로 불필요한 요청을 줄일 수 있다.
async function TaggedData() {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/random`, {
next: { tags: ['books'] }, // 태그 기반 캐시 관리
});
if (!response.ok) {
return <div>오류가 발생했습니다 ...</div>;
}
const data = await response.json();
return <div>{JSON.stringify(data)}</div>;
}
{ next: { tags: ['tag-name'] } }
옵션 동작 원리브라우저에서 초기 요청이 발생하면 데이터 페칭이 실행된다.
캐시에 데이터가 없으므로 MISS
로 판정된다.
백엔드 서버로부터 데이터를 받아와 캐시에 저장(SET)한 뒤 반환한다.
이후 요청에서는 캐시에서 HIT(발견)된 데이터를 반환한다.
별도의 갱신 요청이 없는 한, 캐시된 데이터를 계속 활용한다.
특정 태그를 기반으로 데이터 갱신 요청을 보낸다.
해당 태그와 관련된 데이터가 최신화되고, 캐시에 갱신된 데이터가 저장(SET)된다.
이후 요청은 갱신된 데이터를 반환한다.
Next.js에서 데이터 페칭 요청을 추적하고 로그로 출력하려면, next.config.mjs 파일에 다음과 같이 설정한다.
/** @type {import('next').NextConfig} */
const nextConfig = {
logging: {
fetches: {
fullUrl: true,
},
},
};
export default nextConfig;
콘솔에서 fetch 요청의 상태를 보면, 요청 결과가 다음과 같이 나타날 수 있다.
cache skip
: 캐시가 동작하지 않았음을 나타낸다.
이유: 요청의 cache
옵션이 "no-store"
로 설정되었기 때문이다.
캐싱 옵션을 설정하지 않은 경우
book/random
API 호출과 같이 fetch 요청에 추가적인 캐시 옵션이 설정되지 않았다면, 기본적으로 캐시되지 않는 요청으로 동작한다.
콘솔에는 cache skip
과 함께 auto no cache
라는 메시지가 출력된다.
이 기본 동작은 Next.js의 캐시 기본값이 캐싱하지 않는 것으로 설정되어 있기 때문이다.
fetch의 기본 캐싱 동작이 요청 결과를 무조건 캐싱하는 방식으로 설정되어 있었다.
데이터가 자동으로 캐싱되면서, 일부 불편함을 초래했다. (의도치 않게 오래된 데이터가 반환되는 경우가 발생.)
데이터 캐시의 기본값이 캐싱하지 않는 것으로 변경되었다.
기본 동작이 {cache: "no-store"}
와 동일하게 동작하므로, 명시적으로 캐싱을 활성화하려면 추가적인 옵션을 설정해야 한다.
Next.js 15 버전 이상에서는 캐싱 동작의 명시적인 제어가 가능해졌다.
캐싱되지 않는 기본 동작을 바탕으로 필요한 요청에만 캐싱을 적용.
개발자가 의도적으로 데이터 갱신 주기를 설정하거나, 완전히 캐싱을 비활성화하도록 선택 가능.
이러한 변경 사항은 데이터의 신뢰성과 최신성을 높이고, 개발자의 제어 권한을 확대하는 데 기여한다.
Next.js의 확장된 fetch 메서드와 캐싱 옵션은 데이터 페칭의 효율성을 높이고 서버 부하를 줄이는 데 매우 유용하다.
no-store
: 실시간 데이터.
force-cache
: 정적 데이터.
revalidate
: 주기적 갱신.
tags
: 요청 시 갱신.
Next에서는 이런식으로 fetch메서드를 확장해서 추가적인 옵션을 전달하기만 해도 특정 데이터 페칭의 요청을 영구적으로 캐싱하거나, 시간 기반으로 캐싱하거나, 또는 아예 캐싱하지 않도록 설정할 수 있기 때문에 다양한 사례에 효율적으로 대응할 수 있다.