practical debugging note
Next.js 라우트 핸들러가 배포 후 예전 JSON을 계속 돌려준 날
Next.js Route Handler에서 캐시 의도를 명확히 적지 않아 배포 뒤에도 이전 응답이 보였던 상황을 확인 순서와 수정 기준으로 정리했습니다.
증상은 코드보다 CDN을 먼저 의심하게 만들었다 어느 날 사이드 프로젝트의 /api/profile 응답에 새 필드가 추가됐는데, 운영 화면에서는 계속 예전 JSON이 내려왔다.
- 증상은 코드보다 CDN을 먼저 의심하게 만들었다
- 먼저 확인한 세 가지
- 내가 놓친 코드
- 수정 기준을 정해 두니 다음 문제가 줄었다
- 다음 배포 때 보는 체크리스트
증상은 코드보다 CDN을 먼저 의심하게 만들었다
어느 날 사이드 프로젝트의 /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'
이번 일로 배운 건 캐시가 나쁘다는 게 아니라, 캐시 의도를 코드에 남기지 않으면 나중의 내가 운영 환경에서 추리 게임을 하게 된다는 점이었다.