Next.js
Next.js 라우트 핸들러가 배포 뒤 예전 JSON을 계속 돌려준 캐시 문제
Route Handler의 캐시 정책을 명시하지 않아서 배포 후에도 이전 응답이 계속 내려왔던 상황을 확인 방법과 함께 정리했다.
증상
사이드 프로젝트의 /api/profile 응답에 새 필드가 추가됐는데, 운영 화면에서는 계속 예전 JSON이 내려왔다. 로컬에서는 바로 보였고, 서버에 SSH로 들어가 curl localhost를 해도 새 값이 보였다. 그래서 처음에는 프록시나 CDN 캐시를 의심했다.
헷갈렸던 지점은 Next.js App Router의 Route Handler가 "그냥 API"처럼 느껴졌다는 점이다. 페이지 캐시만 신경 쓰고 Route Handler의 캐시 정책은 제대로 확인하지 않았다.
먼저 세 가지를 확인했다
운영 서버에서 같은 URL을 여러 방식으로 테스트했다.
curl -I https://example.com/api/profile
curl -s https://example.com/api/profile | jq
curl -s -H 'Cache-Control: no-cache' https://example.com/api/profile | jq
여기서 Cache-Control 헤더와 실제 응답 본문을 같이 봤다. 브라우저 개발자 도구만 보고 판단하면 서비스 워커나 탭 캐시까지 섞여서 원인이 흐려졌다.
놓친 코드
문제가 된 코드는 대략 이런 모양이었다.
export async function GET() {
const profile = await getProfile();
return Response.json(profile);
}
데이터가 자주 바뀌지 않는다고 생각해서 별도 옵션을 안 적었는데, 운영에서는 "언제 새로 계산해야 하는지"가 코드만 보고 명확하지 않았다. 결국 이 라우트는 항상 최신 값이 필요하다고 판단해서 아래처럼 바꿨다.
export const dynamic = 'force-dynamic';
export async function GET() {
const profile = await getProfile();
return Response.json(profile, {
headers: {
'Cache-Control': 'no-store',
},
});
}
캐시 정책 기준을 정했다
모든 API에 no-store를 붙이는 식으로 해결하지는 않았다. 내가 정한 기준은 간단하다.
- 로그인 사용자마다 값이 달라지면 캐시하지 않는다.
- 관리자 화면에서 수정한 값이 바로 보여야 하면 캐시하지 않는다.
- 공개 목록처럼 조금 늦어도 괜찮으면 재검증 시간을 명시한다.
- 캐시를 쓸 때는 응답 헤더로 의도를 확인한다.
배포 후 체크리스트
배포 후에는 화면만 새로고침하지 말고, API 응답과 헤더를 같이 확인한다.
curl -i https://example.com/api/profile | sed -n '1,20p'
이번 일로 배운 건 캐시가 나쁘다는 게 아니라, 캐시 의도를 코드에 남기지 않으면 나중의 내가 운영 환경에서 추리 게임을 하게 된다는 점이었다.