당신의 사이트가 글을 데이터베이스에서 읽어 정적 페이지로 굽는다고 하자. 관리 화면에서 글 하나를 내렸고, 다음 배포 뒤에 확인해 보니 그 글이 도로 공개돼 있다. 혹은 반대로, 하루에 상품 몇백 개만 바뀌는 쇼핑몰인데 상품 페이지 수만 쪽이 매일 통째로 다시 만들어지고 데이터베이스 요금이 줄지 않는다. 두 증상은 정반대로 보이지만 원인이 같다.
캐시는 무엇을 보고 되살리거나 지우나
캐시는 키와 태그에 적힌 입력만 본다 — 결과가 실제로 기대는 입력과 키·태그가 어긋나면, 너무 적게 지워 낡은 것이 나가거나 너무 많이 지워 비용이 샌다. 빌드 캐시는 "입력이 같으면 산출물도 같다"를 믿고, 페이지 캐시는 "이 태그가 무효화되면 이 페이지도 낡았다"를 믿는다. 그 믿음은 사람이 적어 준 키와 태그만큼만 정확하다. 캐시는 키 밖에서 무엇이 바뀌었는지 알 길이 없다.
그래서 캐시를 두기 전에 할 일은 하나다. "이 결과는 무엇이 바뀌면 달라지나"를 목록으로 적고, 키·태그·무효화 순서를 그 목록과 맞춘다.

키가 좁으면 왜 낡은 것이 되살아나나
키에 없는 입력이 바뀌어도 캐시는 "같은 입력"으로 본다. 흔한 경우 둘이다.
- 빌드 캐시와 데이터베이스. 모노레포 빌드 도구(Turborepo 등)는 소스 파일·설정·지정한 환경 변수의 해시로 빌드 산출물을 되살린다(Turborepo 캐싱 문서). 빌드 중에 데이터베이스를 읽어 페이지를 굽는 앱이라면, 데이터베이스 내용은 그 해시에 없다. 코드가 안 바뀐 배포에서는 예전 데이터로 구운 페이지가 그대로 다시 나간다. 내린 글이 되살아나고, 시험 데이터가 운영에 실린다.
- 배포 중복 제거. Git 연동 배포는 커밋을 키로 쓰는 경우가 많다. 같은 커밋을 다른 가지에 다시 올리거나 강제로 밀어도, 이미 본 커밋이면 새 배포를 만들지 않을 수 있다. 이때 "건너뛰었다"는 기록조차 안 남으면 연동이 고장 난 것처럼 보인다. 다시 굽고 싶으면 키를 바꿔야 한다 — 새 커밋을 얹는다.
처방은 둘 중 하나다. 데이터를 키에 넣거나(데이터 버전·마지막 수정 시각을 빌드 환경 변수로 넘겨 해시에 포함), 그 작업을 캐시에서 뺀다. 데이터 버전을 키에 넣기 어렵다면 빼는 쪽이 안전하다.
태그가 넓으면 왜 멀쩡한 페이지까지 지워지나
태그 기반 무효화는 "이 태그를 단 캐시 조각을 쓰는 페이지는 전부 낡았다"로 동작한다(Next.js revalidateTag). 상품 상세 페이지가 "카테고리 요약" 조각 하나를 함께 그린다고 하자. 그 조각에 카테고리 태그가 붙어 있으면, 카테고리 안 상품 하나만 바뀌어도 카테고리 전체의 상세 페이지가 함께 지워진다.
지워진 페이지는 다음 방문자가 하나씩 다시 만든다. 그 방문자가 검색 로봇이면, 데이터베이스는 하루 종일 깨어 있다. 처방은 상세 페이지 안의 상위 단위 데이터를 상세 페이지 자신의 태그로 저장하거나, 시간 단위(예: 주)로 끊어 따로 두는 것이다. 태그는 "이 페이지가 무엇에 기대나"가 아니라 "무엇이 바뀌면 이 페이지가 틀리나"로 단다.
무효화는 언제 적용되나
무효화 호출이 곧바로 적용된다고 가정하면 순서가 꼬인다. 어떤 프레임워크는 한 요청 안에서 무효화를 부르면 그 요청의 응답을 돌려준 뒤에 적용한다. 같은 요청 안에서 "무효화 → 바로 다시 굽기"를 하면 다시 굽기는 옛 캐시를 받고 끝나고, 작업이 끝난 뒤에야 캐시가 빈다. 결과는 "굽기를 했는데 페이지가 비어 있다"다.
- 무효화는 별도 요청으로 먼저 보낸다
- 표본 주소 하나로 무효화가 적용됐는지 확인한다
- 그다음에 굽는다
캐시를 두기 전에 적을 목록
아래를 캐시 설정 옆 주석이나 설계 문서에 그대로 붙여 쓴다.
이 캐시가 되살리는 결과: ______
이 결과가 달라지는 입력 (전부):
- 코드 / 설정 / 환경 변수
- 데이터베이스 내용 (어느 표? 어느 행?)
- 시각 (오늘 날짜, 연도 라벨)
- 외부 API 응답
키에 들어간 입력: ______
키 밖에 남은 입력: ______ → 키에 넣는다 / 캐시에서 뺀다 / 남겨도 되는 이유를 적는다
무효화 단위(태그): ______ → 이 태그를 지우면 같이 지워지는 쪽 수: ______
무효화가 적용되는 시점: ______ → 다시 굽기는 그 뒤에
확인 순서:
- 결과가 기대는 입력을 전부 적는다 — 코드 밖의 것(데이터·시각·외부 응답)을 먼저 찾는다
- 키에 들어간 것과 대조해, 키 밖에 남은 입력마다 처방을 정한다
- 태그 하나를 지우면 몇 쪽이 같이 지워지는지 센다 — 실제로 바뀐 쪽 수와 견준다
- 무효화 뒤 표본 주소로 적용을 확인하고 나서 굽는다
- "데이터만 바꾸고 코드는 그대로인 배포"를 한 번 해 보고 결과가 새 데이터인지 본다
우리가 겪은 네 가지
- 빌드 캐시가 옛 데이터를 되살렸다. 데이터베이스에서 글을 읽어 굽는 사이트에서, 실제 글은 1편인데 46편이던 시절의 빌드가 캐시에서 살아났다. 관리 화면에서 내린 글이 다음 배포에 도로 공개될 수 있었다. 그 앱의 빌드를 캐시에서 빼서 고쳤다.
- 넓은 태그가 매일 84%를 지웠다. 한 지역 정보 사이트에서 하루에 바뀌는 항목은 약 1,900건인데, 상세 페이지가 상위 지역 태그 조각을 품고 있어 약 1만 5천 쪽(전체의 84%)이 매일 비워졌다. 바뀐 것만 굽는 새벽 작업 뒤로 나머지를 로봇이 종일 만들며 데이터베이스를 깨웠다.
- 같은 요청 안의 무효화가 굽기보다 늦었다. 같은 사이트에서 데이터베이스가 하루 19시간 깨어 있었다. 무효화와 다시 굽기를 한 라우트 안에서 차례로 불렀는데, 무효화가 응답 뒤에 적용돼 굽기는 옛 캐시만 받았다.
- 같은 커밋은 다시 배포되지 않았다. 미리보기 배포를 다시 띄우려고 같은 커밋을 강제로 밀고 새 가지로도 올렸지만 배포가 생기지 않았고, 건너뛴 기록도 없었다. 새 커밋을 얹자 곧바로 구워졌다.
네 경우 모두 오류는 한 번도 나지 않았다. 캐시는 적힌 키대로 정확히 일했다 — 틀린 것은 키였다.
이전 글순간값에 바로 반응하는 장치는 잡음을 키운다 — 반응하기 전에 무엇을 기준으로 고를지부터 정한다