기술 포스트

ISR 캐시가 안 먹던 날 — fetch revalidate 삽질기


Article

이 블로그는 콘텐츠를 GitHub 저장소에서 API로 가져온다. Vercel에 올리면서 ISR을 1시간 주기로 걸어뒀다. 페이지마다 이 한 줄이면 끝이라고 생각했다.

export const revalidate = 3600;

그런데 배포 후 GitHub API 사용량 그래프가 이상했다. 1시간에 한 번이어야 할 요청이 훨씬 자주 찍히고 있었다. 캐시가 안 먹고 있다는 뜻이다.

삽질의 경로

처음엔 Vercel 쪽을 의심했다. 배포 설정을 뒤지고, 응답 헤더의 x-vercel-cache를 확인했다. STALEMISS가 예상보다 자주 나왔다. 다음엔 GitHub API의 캐시 정책을 의심했다. ETag 때문인가? 아니었다. 요청 자체가 서버에서 자주 나가고 있었으니 응답 쪽 문제가 아니다.

한참을 헤매다가 내 코드를 다시 열었다. lib/github.ts의 fetch 함수. 범인은 거기 있었다.

// lib/github.ts — 문제의 코드
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${GITHUB_TOKEN}` },
  next: { revalidate: 60 }, // ← 예전에 테스트하며 넣어둔 값
});

개발 초기에 콘텐츠 반영을 빨리 확인하려고 60초로 박아둔 값을 그대로 배포한 거다. 페이지에 3600을 걸어도, fetch에 60이 걸려 있으면 그 데이터의 캐시 수명은 60초다. Next.js에서 페이지 레벨 revalidate와 fetch 레벨 next.revalidate는 별개의 레이어이고, 실질적인 재검증 주기는 더 짧은 쪽에 끌려간다.

해결

fetch의 값을 페이지와 일치시켰다. 값을 두 군데서 관리하지 않도록 상수로 뽑았다.

// lib/constants.ts
export const REVALIDATE_TIME = 3600;

// lib/github.ts
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${GITHUB_TOKEN}` },
  next: { revalidate: REVALIDATE_TIME },
});

// app/(pages)/dev/page.tsx
export const revalidate = 3600; // REVALIDATE_TIME과 동일

배포하고 하루를 지켜봤다. API 요청이 시간당 한 번 꼴로 정리됐고 x-vercel-cache: HIT가 정상적으로 찍히기 시작했다.

교훈

캐싱은 레이어가 여러 개다. CDN 캐시, 페이지 레벨 ISR, fetch 레벨 Data Cache. 한 군데만 보고 "설정했으니 됐다"고 넘어가면 다른 레이어가 조용히 그 설정을 무력화한다. 그리고 문제가 생겼을 때 남의 인프라(Vercel, GitHub)부터 의심했는데, 결국 범인은 몇 달 전의 나였다. 디버깅의 첫 순서는 언제나 내가 쓴 코드다.

ISR 캐시가 안 먹던 날 — fetch revalidate 삽질기 — 디코드랩(DCODELAB)