Cloudflare 캐시 유지하며 동적 기능 추가한 2가지 패턴

Cloudflare로 HTML까지 15분 단위로 “아주 세게” 캐싱하면, 트래픽 폭증에도 사이트는 든든해집니다. 문제는 그 다음이죠. “이제 편집 버튼 하나만 달고 싶은데…”, “태그에서 랜덤 글 탐험도 하고 싶은데…” 같은 순간, 캐시를 깨는 순간 모든 이점이 흔들리기 쉽습니다.
이번 글은 캐시를 무력화하지 않고도 동적 기능을 넣는 방법을 소개합니다. 핵심은 서버가 아니라 브라우저(localStorage)와 JavaScript가 일을 하게 만드는 것. 그리고 정말 필요한 일부 요청만 no-cache로 분리하는 전략입니다. (전체 아이디어와 구현 방향은 원문 사례에서 출발했습니다1.)
Cloudflare 적극 캐싱이 ‘기본값’일 때 생기는 고민
Cloudflare에는 “Cache Everything”처럼 HTML까지 캐시해 TTFB를 확 낮추는 공격적인 옵션이 있습니다2. 이런 설정이 들어가면, 공개 페이지는 거의 “정적 사이트처럼” 동작합니다.
그런데 관리자에게만 보이는 편집 링크, 사용자의 클릭에 반응하는 탐색 기능 같은 건 본질적으로 동적이라서, 보통은 “로그인 쿠키에 따라 서버가 다르게 렌더링”하게 됩니다. 이 순간 캐시는 난감해집니다. 캐시 키에 쿠키가 섞이거나, 아예 해당 페이지를 Bypass 하게 되면 성능 이득이 줄어들거든요.
그래서 접근을 바꿉니다. “서버가 다르게 보여주지 말고, 브라우저가 다르게 보이게 하자.”
관리자만 보이는 ‘편집’ 링크: localStorage로 조용히 숨기기
블로그가 Django로 만들어져 있다면, 보통 콘텐츠 타입(글/링크/인용/노트)마다 관리자 편집 화면 URL이 존재합니다. 문제는 이 링크를 공개 페이지에 노출하느냐죠.
여기서 택한 패턴은 단순합니다. HTML에는 편집 링크를 심어두되, 기본 상태에서는 숨김 처리합니다. 그리고 브라우저의 localStorage에 특정 키(예: ADMIN)가 있을 때만 JavaScript로 “편집” 버튼을 보이게 합니다.
이 방식의 재미있는 점은, “관리자 권한 체크”를 서버가 하지 않는다는 겁니다. 그래서 누구나 개발자 도구로 localStorage에 ADMIN 값을 넣으면 버튼을 볼 수 있어요. 하지만 이 버튼은 결국 Django 관리자 페이지로 향하고, 그곳에서 진짜 인증/권한이 걸러줍니다. 즉, 버튼 노출은 UX일 뿐이고 보안은 Django가 책임지는 구조입니다.
운영 관점에서는 꽤 실용적입니다. 캐시된 공개 페이지는 그대로 두고, 나만 체크박스로 “관리자 모드”를 켰다 껐다 하며 편집 동선을 줄일 수 있으니까요.
태그별 랜덤 탐색: “정적 페이지 위에 붙인 게임 버튼” 만들기
두 번째 기능은 더 “동적 기능 같다”는 느낌이 납니다. 태그 페이지에 Random 버튼을 추가해, 클릭하면 그 태그의 임의 콘텐츠로 이동하게 합니다.
여기서 포인트는 “연속 탐험 UX”입니다. 랜덤으로 하나 들어갔는데, 다음도 같은 태그에서 또 랜덤을 돌리고 싶잖아요? 그래서 localStorage에 “지금 어떤 태그 랜덤 탐색 중인지”, “마지막 클릭 시간”을 저장합니다.
버튼 클릭 후 5초 이내에 새 페이지가 로딩되면, 그 페이지 상단에 “현재 태그의 Random 버튼”을 계속 띄워줍니다. 결과적으로 사용자는 태그 안을 핀볼처럼 튕기며 탐색할 수 있고, 사이트는 서버 세션 없이도 “상태가 이어지는 느낌”을 줍니다.
그리고 사용성도 챙깁니다. 해당 태그에 포스트가 5개 미만이면 Random 버튼 자체를 숨겨서 “눌러도 맨날 같은 글 나오는” 허무함을 줄입니다.
/random/태그명/ 엔드포인트: 캐시를 깨지 않고 ‘딱 여기만’ 예외 처리
랜덤 이동은 결국 서버가 “이 태그의 콘텐츠 중 하나를 고른 뒤 그 URL로 보내기”를 해야 합니다. 그래서 /random/태그명/ 같은 엔드포인트를 두고, 요청이 오면 DB에서 하나를 뽑아 302 리다이렉트로 넘깁니다.
중요한 건 이 엔드포인트는 캐시되면 안 된다는 점입니다. 랜덤이 고정되면 랜덤이 아니니까요. 그래서 이 경로만 no-cache로 처리해 매번 새로 계산하게 만듭니다. Cloudflare에서도 관리자 영역이나 특정 동적 경로는 캐시를 우회하도록 분리하는 패턴이 자주 권장됩니다2.
이렇게 하면 공개 페이지(태그/포스트)는 계속 캐시로 빠르게 제공되고, 랜덤 뽑기 계산은 “눌렀을 때만” 최소한으로 발생합니다.
대용량 태그에서의 무작위 선택: CTE로 ‘덜 아프게’ 뽑기
DB에서 랜덤 한 건을 뽑는 건, 데이터가 작을 땐 쉬운데 커지면 갑자기 비용이 커집니다. 흔히 쓰는 ORDER BY RANDOM() 같은 방식은 레코드가 많을수록 무거워지기 쉽습니다.
그래서 대용량 태그에서는 CTE(Common Table Expression) 같은 기법을 활용해 무작위 선택을 더 효율적으로 처리합니다. 구현은 DB 스키마와 인덱스에 따라 달라지지만, 메시지는 하나입니다. “랜덤은 기능이 아니라 쿼리다.” 캐싱을 잘 해놨더라도, 랜덤 엔드포인트 하나가 병목이 되면 전체 체감이 흔들릴 수 있습니다.
캐싱 시대의 동적 기능: 서버가 아니라 ‘클라이언트 상태’로 푼다
정리하면, 이 접근의 장점은 명확합니다.
첫째, 공개 페이지는 계속 캐시를 먹으니 트래픽에 강합니다.
둘째, 동적 기능은 localStorage + JS로 처리해 서버 부담이 거의 없습니다.
셋째, “관리자/일반 사용자”처럼 롤이 갈리는 UX를 서버 렌더링 없이도 깔끔하게 분리할 수 있습니다(보안은 인증이 걸린 진짜 관리자 페이지에서 처리).
마지막으로, 꼭 서버가 필요한 지점(랜덤 선택)만 no-cache로 분리하면 “정적처럼 빠른 사이트”와 “가끔 필요한 동적 기능”을 같이 가져갈 수 있습니다.
강력한 캐싱을 걸어둔 사이트에서 뭔가를 추가할 때마다 “캐시를 풀까?”부터 고민했다면, 다음 질문으로 바꿔보면 좋겠습니다. “이거… 브라우저가 대신 기억해줄 수 없나?”