이번주 공부의 목표는 Cache-Control 지시자부터 Next의 fetch 캐시, ISR까지 캐시 계층을 하나씩 분리해서 "누가 무엇을 얼마나 오래 저장하는지" 알아가는것이 목표
여기서는 하루 21만 건이던 Supabase REST 직접 호출을 왜 /api/catalog/* + CDN 캐시로 옮기면 방문자 수와 DB 부하가 분리되는지, 숫자로 표현할 수 있음
**ISR: 이거 next 에서 사용하는 전체 사이트를 다시 빌드 하지않고도 SSG 된 페이지의 특정 주기에따라 백그라운드에서 렌더링 하여 업데이트 하는거
이주전 장애 조치 중 "정적 데이터를 /api/catalog/* + CDN 5분 캐시로 모아 방문자 수와 DB 부하를 분리" 한 결정.
그 전에는 Framer 코드 컴포넌트가 브라우저에서 anon key로 Supabase REST를 직접 호출해서 캐시가 0겹이였음
max-age는 브라우저와 CDN이 둘다 읽는다.s-maxage는 shared cache만 읽는다. 둘다 있으면 CDN은 s-maxage를 우선하고, 브라우저는 s-maxage를 무시하고 max-age만 본다.둘을 나눈 이유는 무효화할 수 있느냐가 다르기 때문이다.
CDN 캐시는 우리가 purge하거나 revalidate 할 수 있다.
사용자 브라우저에 들어간 캐시는 서버가 지울 방법이 사실상 없다.
그래서 CDN에는 길게, 브라우저에는 짧게 라는 조합이 필요하고, 그걸 표현하려면 값이 두개여야한다.
해당 부분은 더 공부 필요할듯 무슨말인지 모르겠음... 추후 수정예정
"새 데이터를 받아오는 동안 이미 만료된 옛 응답을 보여주는것"
s-maxage=300, stale-while-revalidate=600일 때 흐름
사용자에게 항상 빠르게 느껴지는 이유는: 갱신비용을 사용자 요청경로에서 백그라운드로 옮겼기 때문.
대가는 신선도. 갱신을 촉발한 사용자 한명은 옛 데이터를 받는다. 900초 이후 첫 요청은 느림
no-cache는 왕복은 하지만 본문 비용을 아낀다. no-store은 둘다 안아낌
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=600
s-maxage=300 5분 캐시 결정. 핵심은 DB에 들어오는 요청이 방문자 수와 상관없이 URL당, CDN 엣지당 최대 5분에 1번으로 묶인다는 점. 이것 덕분에 방문자 수와 DB 부하의 분리
max-age=0: 브라우저 캐시는 당장 무효화 할 수 없기에, 가격이나 지원금이 변경되었을때 특정사용자만 옛 가격을 보는 일을 막으려고 브라우저는 매번 CDN에 묻게 한다.
stale-while-revalidate=600: 사용자가 원 서버를 기다리는 일 을 제거한다.
최악의 경우 옛 데이터가 보이는 시간은 대략 s-maxage + swr = 15분. 최대 15분 늦게 반영돼도 괜찮은가? 를 생각해야 했고, 괜찮다. 만약 변경을 더 빨리 반영해야 한다면. swr을 줄이거나 변경 시점에 CDN purge 나 on-demand revalidate를 쓰면 된다.
stale-if-error=86400. 이것은 원 서버나 DB가 에러를 내도 CDN이 옛 응답으로 버터게 해줌. 장애 조치를 위해 필요하다. 다만 CDN마다 지원 여부가 달라 CDN문서 확인필요
Next.js App Router Route Handler에서 사용법
return Response.json(data, {
headers: { 'Cache-control': 'public, max-age=0, s-maxage=300, stale-while-revalidate=600' },
});
Framer -> Supabase REST 직접 호출에는 왜 캐시를 걸 곳이 없었는가.....
1. 응답 헤더를 쓰는 주체가 Supabase 이기 때문에. Cache-Control은 응답 헤더라서 원 서버가 정한다.
원 서버가 Supabase 였으니 우리 쪽에는 헤더를 붙일 코드가 한 줄도 없었다.
2. 중간에 우리가 소유한 계층이 없었다. 브라우저에서 곧바로 xx.supabase.co/rest/v1/...으로 갔기 때문에 CDN을 끼워 넣을 자리가 없었음
3. 요청에 Authorization: Bearer <anon key> 가 붙는다. 중간에 shared cache가 있더라도 public이나 s-maxage없이 저장할 수 없다.
4. 브라우저 캐시조차 명시적인 유효기간이 없으면 거의 쓰이지 안흔ㄴ다.
결과적으로 방문자 1명의 페이지 로드 = 쿼리 N개가 그대로 Postgres까지 도달했다.
캐시가 0겹이니 방문자 수와 DB 부하가 1:1 이였고... 터지게 되었다.
/api/catalog/* 를 두면서 응답 헤더의 주인을 변경했고. 그러므로 s-maxage를 쓸 수 있게 된것이다.