Ghost 셀프 호스팅 블로그 TTFB 3.7초 → 0.4초: Cloudflare Cache Rule로 87% 속도 개선
Ghost CMS는 기본적으로 HTML에 max-age=0을 설정해서 Cloudflare 캐시가 작동하지 않습니다. Cache Rule 하나로 TTFB를 3.7초에서 0.4초로 줄인 방법을 공유합니다.
1. 문제 상황
셀프 호스팅으로 운영 중인 Ghost 블로그가 체감상 너무 느려졌습니다. 페이지를 열 때마다 3-4초씩 기다려야 했고, 모바일에서는 더 심했습니다.
구성은 이렇습니다:
사용자 → Cloudflare (CDN/SSL) → Nginx Proxy Manager → Ghost (Docker) → MySQL
서버 스펙도 나쁘지 않았습니다:
| 항목 | 값 |
|---|---|
| 서버 | Oracle Cloud (무료 티어) |
| CPU | 4 OCPU (ARM) |
| RAM | 24GB |
| OS | Ubuntu 24.04 |
| Ghost | Docker (6.19.1) |
curl로 TTFB(Time to First Byte)를 측정해보니 충격적이었습니다:
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
https://blog.example.dev/
TTFB: 3.165s
Total: 3.606s
TTFB가 3초가 넘습니다. Google은 TTFB 0.8초 이하를 권장하는데, 4배 가까이 초과하고 있었습니다. SEO에도 직접적인 악영향을 주는 수치입니다.
2. 원인 분석: 레이어별 병목 추적
느린 원인을 찾기 위해 요청 경로의 각 레이어를 하나씩 테스트했습니다.
2.1 Ghost 직접 테스트 (프록시 우회)
서버에 SSH로 접속해서 Ghost 컨테이너에 직접 요청합니다:
# Ghost 직접 (localhost:2368)
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" http://localhost:2368/
TTFB: 0.002069s # 2ms!
Ghost 자체는 2ms로 매우 빠릅니다. Ghost가 느린 게 아닙니다.
2.2 Nginx Proxy Manager 경유 테스트
# NPM 경유 (localhost:80)
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" \
http://localhost:80 -H "Host: blog.example.dev"
TTFB: 0.000685s # 0.7ms
NPM도 문제없습니다. 서버 내부는 완벽하게 빠릅니다.
2.3 서버에서 Cloudflare 경유 테스트
서버 자체에서 외부 URL(Cloudflare 경유)로 요청합니다:
# 서버 → Cloudflare → 서버 (루프백)
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" https://blog.example.dev/
TTFB: 0.199531s # 200ms
서버에서 Cloudflare를 거쳐 돌아오는 시간은 200ms입니다. 합리적인 수치입니다.
2.4 외부에서 접속 테스트
# 한국에서 접속
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" https://blog.example.dev/
TTFB: 3.165861s # 3.1초!
외부에서 접속할 때만 3초가 걸립니다. 서버 내부(2ms) → 외부(3100ms)의 1500배 차이는 Cloudflare 구간에서 발생하고 있습니다.
2.5 응답 헤더 분석
원인의 결정적 단서는 응답 헤더에 있었습니다:
curl -sI https://blog.example.dev/ | grep -i 'cache'
cache-control: public, max-age=0
cf-cache-status: DYNAMIC
cf-ray: 9d277e433b00a3b9-SEA
세 가지 문제가 동시에 보입니다:
| 헤더 | 값 | 의미 |
|---|---|---|
cache-control |
public, max-age=0 |
Ghost가 "캐시하지 마세요"라고 응답 |
cf-cache-status |
DYNAMIC |
Cloudflare가 이 응답을 캐시하지 않음 |
cf-ray |
...-SEA |
시애틀(Seattle) POP을 경유 |
3. 근본 원인: Ghost의 max-age=0 + Cloudflare의 기본 동작
3.1 Ghost는 HTML을 캐시하지 않습니다
Ghost CMS는 기본적으로 모든 HTML 응답에 cache-control: public, max-age=0을 설정합니다. Ghost 설정 파일에서 caching.frontend.maxAge 값을 변경할 수 있지만, Docker 환경에서는 환경변수로 설정하기 까다롭고, Cloudflare Cache Rule에서 제어하는 것이 더 유연합니다.
max-age=0이 의미하는 것:
"이 응답은 공개(public)지만, 유효 기간은 0초입니다.
매번 서버에 다시 확인하세요."
3.2 Cloudflare는 HTML을 기본 캐시하지 않습니다
Cloudflare의 기본 캐시 동작에 따르면, Cloudflare CDN은 HTML과 JSON을 기본적으로 캐시하지 않습니다. 이미지, JS, CSS 같은 정적 파일만 캐시합니다.
즉, 두 가지가 겹칩니다:
Ghost: "max-age=0이니까 캐시하지 마세요"
Cloudflare: "HTML은 원래 캐시 안 합니다"
→ 결과: 모든 HTML 요청이 매번 Origin 서버까지 도달
3.3 Cloudflare POP 라우팅 문제
cf-ray 헤더에서 SEA(시애틀)가 보입니다. 서버는 한국에 있는데, Cloudflare가 시애틀 POP을 통해 라우팅하고 있었습니다.
# 서버 위치 확인
curl -s https://ipinfo.io/152.69.xxx.xxx | jq '.city, .country'
"Chuncheon"
"KR"
서버는 한국(춘천)인데, 요청이 시애틀을 경유합니다. 캐시가 없으니 매 요청마다 이 왕복이 발생합니다:
캐시 없을 때 (Before):
사용자(한국) → 시애틀 POP → 한국(Origin) → 시애틀 POP → 사용자(한국)
= 태평양 2번 왕복 = 3.7초
이것이 TTFB 3초의 정체입니다.
3.4 시애틀 경유는 왜 발생하나요?
Cloudflare Free 플랜은 ISP와의 피어링(peering) 계약에 따라 가장 가까운 POP이 아니라 네트워크 비용이 낮은 POP으로 라우팅합니다. 한국 일부 ISP에서 시애틀로 빠지는 건 알려진 현상입니다.
이 문제 자체를 해결하려면 Cloudflare Pro 플랜($20/월) 이상이 필요합니다. 하지만 캐시를 활성화하면 시애틀 경유의 영향이 최소화됩니다:
캐시 있을 때 (After, HIT):
사용자(한국) → 시애틀 POP(여기서 캐시 응답!) → 사용자(한국)
= 태평양 1번 왕복 = 0.4초
캐시가 없으면 시애틀 → 한국(Origin) → 시애틀 왕복이 추가되지만, 캐시 HIT이면 시애틀 POP에서 바로 응답하기 때문에 Origin까지 갈 필요가 없습니다. 시애틀을 경유하긴 하지만, 거기서 바로 돌아오는 것입니다.
요약: 병목 지점 비교
| 측정 구간 | TTFB | 상태 |
|---|---|---|
| Ghost 직접 (localhost:2368) | 2ms | 정상 |
| NPM 경유 (localhost:80) | 0.7ms | 정상 |
| 서버 → Cloudflare → 서버 | 200ms | 정상 |
| 외부 → Cloudflare → 서버 | 3,165ms | 문제! |
4. 해결 방법: Cloudflare Cache Rule 설정
해결책은 간단합니다. Cloudflare에 HTML 캐시 규칙을 추가하면 됩니다.
4.1 Cache Rule 생성
Cloudflare Dashboard → Caching → Cache Rules → Create rule
Rule 설정:
| 항목 | 값 |
|---|---|
| Rule name | Ghost Blog HTML Cache |
Expression을 직접 편집("Edit expression")해서 아래와 같이 입력합니다:
(http.host eq "blog.example.dev" and not starts_with(http.request.uri.path, "/ghost/"))
이 expression은 두 가지를 동시에 처리합니다:
blog.example.dev호스트의 요청만 캐시 대상으로 지정/ghost/(관리자 페이지) 경로를 명시적으로 제외
4.2 Cache Eligibility 설정
"Eligible for cache" 를 선택합니다. 이것이 Cloudflare에게 "이 응답을 캐시해도 됩니다"라고 알려주는 설정입니다.
4.3 Edge TTL 설정
가장 중요한 설정입니다. Ghost가 max-age=0을 보내기 때문에, Origin의 cache-control 헤더를 무시해야 합니다.
Edge TTL:
● Ignore cache-control header and use this TTL
Input time-to-live (TTL): 1 day
"Ignore cache-control header and use this TTL" 을 선택하고 24시간(1 day) 으로 설정합니다. 자동 캐시 퍼지를 함께 설정하면 TTL을 길게 잡아도 새 글 발행 시 즉시 반영됩니다.
이 옵션을 선택하는 이유:
| 옵션 | 설명 | Ghost에서의 동작 |
|---|---|---|
| Use cache-control header if present, bypass if not | Origin 헤더 존중 | max-age=0이므로 캐시 안 됨 |
| Use cache-control header if present, use default TTL if not | Origin 헤더 존중, 없으면 기본값 | max-age=0이므로 캐시 안 됨 |
| Ignore cache-control header and use this TTL | Origin 헤더 무시 | 강제로 2시간 캐시 |
Ghost의 max-age=0을 무시하려면 세 번째 옵션만 작동합니다.
4.4 Browser TTL 설정
Browser TTL:
● Respect origin TTL
"Respect origin TTL" 을 선택합니다. Ghost가 보내는 원래 cache-control: public, max-age=0이 그대로 브라우저에 전달되어, 브라우저가 페이지를 로컬에 캐시하지 않습니다.
이렇게 하면 캐시 퍼지 후 브라우저에서 새로고침만 하면 즉시 최신 버전이 보입니다. "Override origin"으로 브라우저 캐시를 설정하면, 퍼지를 해도 브라우저 캐시 기간 동안은 하드 리프레시(Cmd+Shift+R)를 해야만 최신 버전을 볼 수 있어서 불편합니다.
| 설정 | Edge TTL | Browser TTL | 의미 |
|---|---|---|---|
| 권장값 | 24시간 | Respect origin (max-age=0) | Edge에서 24시간 캐시, 브라우저 캐시 없음 |
4.5 /ghost/ 관리자 페이지 제외가 필수인 이유
반드시 /ghost/ 경로를 expression에서 제외해야 합니다. 선택 사항이 아닙니다.
Ghost Admin은 cache-control: private, no-cache, no-store를 보내서 캐시되면 안 됩니다. 하지만 우리가 설정한 "Ignore cache-control header and use this TTL" 옵션은 private 헤더까지 무시하고 강제로 캐시합니다. 별도의 bypass Cache Rule을 만들어도, "Ignore cache-control" 설정이 우선되어 bypass가 작동하지 않습니다.
# /ghost/를 제외하지 않으면 이렇게 됩니다:
curl -sI https://blog.example.dev/ghost/ | grep cf-cache-status
cf-cache-status: HIT # ← 관리자 페이지가 캐시됨! 위험!
# /ghost/를 제외하면:
cf-cache-status: DYNAMIC # ← 정상 (캐시 안 됨)
그래서 expression에 not starts_with(http.request.uri.path, "/ghost/")를 반드시 포함해야 합니다. 이렇게 하면 /ghost/* 경로는 Cache Rule에 매칭되지 않아, Ghost의 원래 private 헤더가 존중됩니다.
4.6 Deploy
"Deploy" 버튼을 클릭하면 즉시 적용됩니다. Cloudflare의 글로벌 네트워크에 규칙이 전파되는 데 수 초 정도 걸립니다.
5. 결과 확인
5.1 첫 번째 요청 (Cache MISS)
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" -D - \
https://blog.example.dev/ 2>&1 | grep -iE '(TTFB|cf-cache-status|cache-control)'
cache-control: public, max-age=0
cf-cache-status: MISS
TTFB: 1.156168s
첫 요청은 캐시가 없으므로 MISS입니다. MISS일 때는 여전히 Origin 서버까지 요청이 가기 때문에 네트워크 상태에 따라 1~3초 정도 걸립니다 (이전과 비슷). cache-control이 max-age=0인 것은 Ghost의 원래 헤더가 그대로 전달된 것이고(Browser TTL: Respect origin), Cloudflare Edge에서는 "Ignore cache-control" 설정에 따라 24시간 캐시합니다.
5.2 두 번째 요청 (Cache HIT)
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" -D - \
https://blog.example.dev/ 2>&1 | grep -iE '(TTFB|cf-cache-status|cache-control)'
cache-control: public, max-age=0
cf-cache-status: HIT
TTFB: 0.433461s
두 번째 요청부터 HIT! TTFB 0.43초로 Cloudflare Edge에서 직접 응답합니다. cache-control은 여전히 max-age=0이지만, 이건 브라우저용 헤더이고, Cloudflare Edge에서는 이미 24시간 캐시로 설정되어 있습니다.
5.3 Before/After 비교
| 측정 | Before | After (HIT) | 개선율 |
|---|---|---|---|
| TTFB | 3.17s | 0.43s | 87% 감소 |
| Total | 3.61s | 0.47s | 87% 감소 |
| cf-cache-status | DYNAMIC | HIT | 캐시 적용 |
| cache-control | max-age=0 | max-age=0 | 브라우저 캐시 없음 (Edge만 캐시) |
5.4 연속 테스트로 안정성 확인
for i in 1 2 3; do
echo "--- Run $i ---"
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" https://blog.example.dev/
done
--- Run 1 ---
TTFB: 0.433461s
--- Run 2 ---
TTFB: 0.410674s
--- Run 3 ---
TTFB: 0.425103s
일관되게 0.4초대를 유지합니다.
6. 보너스: Ghost 업데이트
속도 개선과 함께 Ghost도 최신 버전으로 업데이트했습니다.
6.1 백업 먼저
# 백업 스크립트 실행
bash ~/scripts/backup.sh
=== Ghost 백업 시작 ===
[1/4] MySQL 데이터베이스 백업 중...
→ ghost-db-20260224_091222.sql 생성 완료
[2/4] Ghost 콘텐츠 백업 중...
→ ghost-content-20260224_091222.tar.gz 생성 완료
[3/4] Docker 설정 백업 중...
→ docker-compose-20260224_091222.yml 생성 완료
[4/4] GitHub에 백업 업로드 중...
→ GitHub push 완료
=== 백업 완료 ===
6.2 docker-compose.yml 이미지 태그 변경
# Before
image: ghost:6-alpine # ← 6.x 최신 (6.19.1)
# After
image: ghost:6.19.2-alpine # ← 특정 버전 고정
6-alpine처럼 메이저 버전 태그를 쓰면 docker pull 시 의도치 않게 버전이 올라갈 수 있습니다. 특정 버전을 고정하는 것이 안전합니다.
6.3 Pull & Restart
cd ~/docker/ghost
# 새 이미지 다운로드
docker compose pull ghost
# 컨테이너 재시작 (ghost-db는 그대로)
docker compose up -d ghost
Container ghost-db Running
Container ghost Recreate
Container ghost Recreated
Container ghost Started
6.4 버전 확인
docker compose exec ghost ghost version
Ghost-CLI version: 1.28.4
Ghost version: 6.19.2 (at /var/lib/ghost)
6.19.1 → 6.19.2 업데이트 완료입니다. 6.19.1에는 Content API의 SQL Injection 보안 패치가 포함되어 있으니, 아직 업데이트하지 않으셨다면 반드시 해주세요.
7. 핵심 개념 정리
Cloudflare 캐시 상태 (cf-cache-status)
| 상태 | 의미 | TTFB 영향 |
|---|---|---|
HIT |
Edge 캐시에서 응답 | 빠름 (< 1초) |
MISS |
캐시 없음, Origin에서 가져옴 | 느림 (Origin 응답 시간) |
DYNAMIC |
캐시 대상이 아님 | 항상 느림 |
EXPIRED |
캐시 만료, Origin에서 재검증 | MISS와 비슷 |
REVALIDATED |
캐시 만료 후 304 확인 | HIT보다 약간 느림 |
Edge TTL vs Browser TTL
Edge TTL (Cloudflare 서버 캐시)
├── 24시간: Cloudflare의 전 세계 POP에 캐시
├── 캐시 히트 시 Origin 서버에 요청하지 않음
├── Cloudflare Dashboard 또는 API로 수동/자동 Purge 가능
└── 자동 퍼지 설정 시 TTL을 길게 잡아도 안전
Browser TTL (사용자 브라우저 캐시)
├── Respect origin: Ghost의 max-age=0이 그대로 전달
├── 매번 Cloudflare Edge에 요청 (HIT이면 0.4초)
└── 캐시 퍼지 후 새로고침만 하면 즉시 최신 버전 확인 가능
Ghost 캐시 관련 헤더 동작
| 경로 | cache-control | Cloudflare 동작 |
|---|---|---|
/ (홈) |
public, max-age=0 |
Cache Rule 없으면 DYNAMIC |
/post-slug/ (포스트) |
public, max-age=0 |
Cache Rule 없으면 DYNAMIC |
/ghost/ (관리자) |
no-cache 또는 private |
항상 DYNAMIC (캐시 안 됨) |
/assets/* (JS/CSS) |
public, max-age=31536000 |
기본 캐시됨 |
/content/images/* |
public, max-age=31536000 |
기본 캐시됨 |
8. 베스트 프랙티스
Ghost 셀프 호스팅 + Cloudflare 캐시 체크리스트
- [ ] Cache Rule 생성: Hostname +
/ghost/제외 expression으로 블로그 캐시 활성화 - [ ] Edge TTL: "Ignore cache-control header" + 24시간 (자동 퍼지와 함께)
- [ ] Browser TTL: "Respect origin TTL" (브라우저 캐시 없음, 퍼지 즉시 반영)
- [ ] /ghost/ 제외 (필수): expression에서 관리자 경로를 반드시 제외 ("Ignore cache-control"이
private헤더도 무시하기 때문) - [ ] docker-compose.yml: Ghost 이미지 태그를 특정 버전으로 고정 (
ghost:6.19.2-alpine) - [ ] 업데이트 전 백업: DB + 콘텐츠 + Docker 설정 백업 필수
- [ ] TTFB 모니터링:
curl -w "%{time_starttransfer}"으로 정기적으로 확인
새 글 발행 후 캐시 갱신
캐시를 설정하면 필연적으로 따라오는 질문이 있습니다: 새 글을 발행하면 어떻게 되나요?
Edge TTL을 2시간으로 설정했다면, 새 글 발행 후 최대 2시간까지 이전 캐시가 남아있을 수 있습니다. 이 문제를 해결하는 방법은 세 가지입니다.
방법 1: 수동 퍼지 (Purge Everything)
Cloudflare Dashboard → Caching → Overview → Purge Everything
"전체 퍼지"란 Cloudflare 전 세계 모든 POP(300곳 이상)에 저장된 모든 캐시를 즉시 삭제하는 것입니다.
Purge Everything 실행
→ 시애틀 POP 캐시: 삭제
→ 서울 POP 캐시: 삭제
→ 도쿄 POP 캐시: 삭제
→ ... (전 세계 300+ POP 모두)
→ 다음 방문자: MISS (Origin에서 새로 가져옴) → 새 글 보임
간단하지만, 글을 쓸 때마다 대시보드에서 직접 클릭해야 합니다.
방법 2: 특정 URL만 퍼지
전체가 아니라 홈페이지, 새 글 URL 등 특정 URL만 캐시를 삭제할 수 있습니다:
# Cloudflare API로 특정 URL 캐시 퍼지
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer {api_token}" \
-H "Content-Type: application/json" \
--data '{"files":["https://blog.example.dev/", "https://blog.example.dev/sitemap.xml"]}'
다른 캐시된 페이지에 영향을 주지 않아서 효율적입니다.
방법 3: 사이트 변경 시 자동 캐시 퍼지 + 워밍 (권장)
Ghost Webhook + n8n을 조합해서, 사이트에 변경이 생길 때마다 자동으로 캐시를 퍼지하고 워밍할 수 있습니다.
구성 요소:
- Ghost Webhook:
Site changed (rebuild)이벤트 → n8n Webhook URL - n8n 워크플로우: Webhook → Cloudflare Purge API → 홈페이지 GET 요청(워밍)
- Cloudflare API Token: Zone > Cache Purge > Purge 권한
Site changed (rebuild) 이벤트는 글 발행/수정뿐 아니라 페이지 변경, 설정 변경, 네비게이션 수정 등 사이트 공개 페이지에 영향을 주는 모든 변경을 커버합니다. 개별 이벤트(Post published, Published post updated 등)를 하나하나 등록할 필요 없이 하나로 처리할 수 있습니다.
사이트 변경 → Ghost Webhook → n8n → Cloudflare Purge API → 홈페이지 워밍
→ 워밍이 닿은 POP: 즉시 HIT (0.4초)
→ 다른 POP의 첫 방문자: MISS (1~3초) → 두 번째부터 HIT
Webhook URL 보안: n8n webhook 경로에 랜덤 문자열을 추가하면(/webhook/ghost-cache-purge-{random}) URL을 추측해서 악의적으로 퍼지를 트리거하는 것을 방지할 수 있습니다.
캐시 워밍의 한계: n8n 서버에서 홈페이지에 GET 요청을 보내면, 그 서버가 연결된 Cloudflare POP의 캐시만 워밍됩니다. 다른 POP을 사용하는 방문자의 첫 요청은 여전히 MISS입니다. 하지만 24시간 TTL이니 한 번 MISS를 겪으면 이후 24시간(또는 다음 퍼지까지)은 계속 HIT입니다.
Edge TTL 선택 가이드
| 발행 빈도 | 자동 퍼지 | 권장 Edge TTL | 이유 |
|---|---|---|---|
| 하루 1-2회 | 있음 | 24시간 | 퍼지가 자동이니 TTL을 길게 잡아 캐시 히트율 최대화 |
| 하루 1-2회 | 없음 | 2시간 | 최악의 경우 2시간만 기다리면 새 글 반영 |
| 주 1-2회 | 있음/없음 | 24시간 | 발행이 드물어 TTL 영향 적음 |
| 실시간 업데이트 필요 | 필수 | 24시간 | 자동 퍼지 없으면 캐시 자체를 쓰기 어려움 |
9. 설정 확인 및 테스트
모든 설정이 끝난 후 아래 명령어로 정상 작동을 확인할 수 있습니다.
9.1 캐시 상태 확인
# cf-cache-status와 TTFB 확인
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s\n" -D - \
https://blog.example.dev/ 2>&1 | grep -iE '(TTFB|cf-cache-status|cache-control)'
정상 결과 (HIT):
cache-control: public, max-age=0
cf-cache-status: HIT
TTFB: 0.433461s
9.2 어떤 Cloudflare POP을 경유하는지 확인
# cf-ray 헤더에서 POP 코드 확인 (마지막 3글자)
curl -sI https://blog.example.dev/ | grep cf-ray
cf-ray: 9d34c7c0699f2f3a-ICN # ICN = 인천(한국)
주요 POP 코드: ICN(인천), NRT(도쿄), LAX(로스앤젤레스), SEA(시애틀)
9.3 자동 퍼지 + 캐시 워밍 테스트
# n8n webhook을 직접 호출해서 퍼지 테스트
curl -s -X POST https://n8n.example.dev/webhook/ghost-cache-purge-{your-random-string} \
-H "Content-Type: application/json" \
-d '{}'
정상 결과:
Cache purged and warmed successfully
9.4 퍼지 후 캐시 재생성 확인
# 퍼지 직후: MISS (캐시가 삭제됨)
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s " \
https://blog.example.dev/ 2>&1 | head -1
# 두 번째 요청: HIT (캐시 재생성됨)
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s " \
https://blog.example.dev/ 2>&1 | head -1
9.5 TTFB 상세 분석
# DNS, TCP 연결, TLS, TTFB 각 단계별 시간 측정
curl -o /dev/null -s -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
https://blog.example.dev/
10. FAQ (자주 묻는 질문)
Q: Ghost Admin(/ghost/) 페이지도 캐시되나요?
A: Cache Rule expression에서 /ghost/를 제외했다면 캐시되지 않습니다. 이 제외는 필수입니다. "Ignore cache-control header" 옵션은 Ghost Admin의 private 헤더까지 무시하고 캐시하기 때문에, expression에서 명시적으로 제외하지 않으면 관리자 페이지가 캐시되는 문제가 발생합니다.
Q: 새 글을 발행하면 바로 반영되나요?
A: Ghost Webhook(Site changed)으로 자동 퍼지를 설정했다면, 글 발행 즉시 캐시가 퍼지되고 홈페이지가 워밍됩니다. 자동 퍼지를 설정하지 않았다면, Edge TTL이 만료될 때까지(최대 24시간) 이전 캐시가 남아있을 수 있습니다.
Q: 캐시 퍼지를 하면 새 버전이 자동으로 캐시되나요?
A: 아닙니다. 퍼지는 캐시를 삭제만 합니다. 퍼지 직후 첫 번째 방문자가 접속할 때 Origin에서 새로 가져오면서(MISS, ~1.2초) 새 캐시가 생성됩니다. 두 번째 방문자부터는 HIT(0.4초)입니다.
Q: Cloudflare가 2시간마다 자동으로 캐시를 업데이트하나요?
A: 아닙니다. Edge TTL 2시간은 "2시간 동안 캐시를 유효하게 취급한다"는 뜻입니다. 2시간이 지나면 캐시가 만료(EXPIRED)되고, 다음 방문자가 접속할 때 비로소 Origin에서 새로 가져옵니다. Cloudflare가 능동적으로 업데이트하는 것이 아니라, 방문자의 요청이 트리거입니다.
Q: 한국에서 접속하는데 미국 POP을 경유하는 이유는?
A: Cloudflare Free 플랜의 한계입니다. ISP(KT, SKT 등)와 Cloudflare 간의 네트워크 피어링 계약에 따라, 일부 한국 ISP 트래픽이 미국 POP(LAX, SEA 등)으로 라우팅될 수 있습니다. Pro 플랜($20/월) 이상에서는 한국 POP(ICN)을 직접 사용할 수 있습니다. 하지만 캐시 HIT 시에는 미국 POP에서 바로 응답하므로(0.4초) Origin까지 갈 필요가 없어 영향이 최소화됩니다.
Q: 글을 수정했는데 브라우저에서 이전 버전이 보여요.
A: Browser TTL을 "Respect origin"으로 설정했다면 새로고침만 하면 최신 버전이 보입니다. 만약 "Override origin"으로 브라우저 캐시를 설정한 경우에는, 해당 시간 동안 Cmd+Shift+R(Mac) 또는 Ctrl+Shift+R(Windows)로 하드 리프레시해야 합니다.
Q: 로그인한 멤버에게도 캐시된 페이지가 보이나요?
A: 멤버십 기능을 적극 사용하신다면, /members/ 경로를 Cache Rule에서 제외하는 것을 권장합니다. 일반 블로그 사용에서는 큰 문제가 되지 않습니다.
Q: Edge TTL을 더 길게 해도 되나요?
A: 네. 자동 퍼지를 설정했다면 24시간으로 설정하는 것을 권장합니다. 캐시 히트율이 높아져서 더 빠르고 Origin 서버 부하도 줄어듭니다. 자동 퍼지 없이 TTL을 길게 하면 새 글 반영이 그만큼 늦어지니 주의하세요.
Q: ghost:6-alpine 대신 ghost:6.19.2-alpine처럼 버전을 고정해야 하나요?
A: 권장합니다. 6-alpine은 docker pull 시 6.x 최신 버전으로 업데이트될 수 있어서, 의도하지 않은 버전 변경이 발생할 수 있습니다. 특정 버전을 고정하고, 업데이트는 백업 후 수동으로 진행하는 것이 안전합니다.
11. 참고 자료
- Increase the performance of your self-hosted ghost blog with Cloudflare caching - Ghost Forum
- Ghost Self-Hosting Guide - Ghost Docs
- Default Cache Behavior - Cloudflare Docs
- Origin Cache Control - Cloudflare Docs