플러그인 없이 워드프레스 5배 빠르게 — Nginx FastCGI 캐시 실전


6편에서 SSL을 붙여 주소창의 자물쇠까지 채웠습니다. 이제 사이트는 안전합니다. 그런데 빠른 건 아니었습니다. 워드프레스는 방문자가 페이지를 열 때마다 PHP를 돌리고 데이터베이스에 몇 번씩 질문을 던져 HTML을 새로 조립합니다. 방문자가 늘면 이 조립 작업이 그대로 서버 부하가 되고, 느려지고, 이탈로 이어집니다. 이번 편에서는 플러그인 하나 없이, Nginx 설정만으로 이 문제를 잡습니다. 그리고 그 결과를 제 Vultr 서울 서버에서 직접 측정한 숫자로 보여드립니다.

결론부터. Nginx FastCGI 캐시를 적용하자, 로그인하지 않은 방문자 기준 서버 응답 시간(TTFB)이 153.98ms → 평균 28.7ms로 떨어졌습니다. 약 5.4배 빠르고, 가장 빠른 순간은 7배 이상입니다. 플러그인은 쓰지 않았고, sitemap·관리자 페이지는 캐시에서 제외했습니다.

워드프레스가 느린 진짜 이유

정적인 HTML 파일 하나를 그냥 돌려주는 건 서버 입장에서 아주 쉬운 일입니다. 문제는 워드프레스가 정적이지 않다는 것입니다. 방문자가 글 하나를 열 때 서버 안에서는 대략 이런 일이 벌어집니다.

PHP-FPM이 깨어나 테마와 플러그인 코드를 읽고 → 데이터베이스(MariaDB)에 “이 글의 제목은? 본문은? 댓글은? 최근 글 목록은?”을 여러 번 묻고 → 그 답을 모아 HTML을 완성한 뒤 → 비로소 방문자에게 넘깁니다. 방문자 한 명 한 명, 매번 이 과정을 처음부터 반복합니다. 어제 열었던 글을 오늘 다른 사람이 열어도 서버는 “처음 보는 요청”처럼 똑같이 다시 조립합니다.

여기서 낭비가 보입니다. 로그인하지 않은 방문자에게는 같은 글이 늘 똑같은 모습으로 보입니다. 그런데도 매번 새로 만들 이유가 없죠. 한 번 만든 결과물을 저장해 두고, 다음 사람에게는 저장본을 그대로 내주면 됩니다. 그게 캐시입니다.

FastCGI 캐시가 하는 일 — MISS와 HIT

Nginx FastCGI 캐시는 완성된 HTML 페이지를 통째로 서버 디스크에 저장하는 방식입니다. 동작은 두 가지 상태로 요약됩니다.

상태무슨 일이 벌어지나
MISS저장본이 없음 → PHP·MariaDB가 페이지를 새로 조립하고, Nginx가 그 결과를 저장
HIT저장본이 있음 → PHP·DB를 아예 건너뛰고 Nginx가 저장본을 즉시 반환
BYPASS규칙에 걸림(로그인·관리자 등) → 캐시하지 않고 항상 새로 조립

첫 방문자 한 명이 MISS로 페이지를 “데워 놓으면”, 그 뒤로 오는 모든 방문자는 HIT로 빠르게 받습니다. 트래픽의 대부분은 로그인하지 않은 방문자라, 사실상 방문자 대다수가 HIT의 혜택을 봅니다.

플러그인 캐시와 뭐가 다른가? WP Super Cache 같은 플러그인도 원리는 비슷하지만, 그 경우엔 요청이 PHP까지 일단 들어온 뒤 걸러집니다. FastCGI 캐시는 PHP에 도달하기도 전에 Nginx 단계에서 끝납니다. 문지기가 문 앞에서 처리하느냐, 안까지 들여보낸 뒤 처리하느냐의 차이입니다.

실측 — 내 Vultr 서울 서버에서 TTFB 154ms → 29ms

측정 환경

항목사양
위치 / 제공사서울 리전 · Vultr
OSUbuntu 24.04 LTS
웹 스택Nginx + PHP 8.3(PHP-FPM) + MariaDB
RAM2GB (측정 시 여유 약 1.3GiB)
vCPU1 vCPU

측정은 curl로 홈페이지를 반복 호출해 TTFB(첫 바이트가 돌아오기까지의 시간)를 8회 재는 방식으로 했습니다. “적용 전”은 캐시를 비운 직후의 MISS 응답, “적용 후”는 캐시가 적중한 HIT 응답입니다.

구분상태TTFB
적용 전MISS — PHP+MariaDB 렌더링153.98ms
적용 후 (최저)HIT — Nginx 저장본 반환20.5ms
적용 후 (평균)HIT · 8회 평균28.7ms
적용 후 (최고)HIT46.1ms

숫자 해석

평균만 봐도 153.98ms에서 28.7ms로, 약 5.4배 빨라졌습니다. TTFB 기준 81%가 사라진 셈입니다. 가장 빠른 순간(20.5ms)은 적용 전 대비 7배가 넘습니다. 눈에 보이는 화면이 극적으로 바뀌진 않지만, 이 “서버가 반응을 시작하기까지의 시간”이 바로 구글이 페이지 속도(Core Web Vitals)를 평가할 때 먼저 보는 지점이고, 방문자가 “이 사이트 뜨는 게 왜 이렇게 느려” 하고 뒤로가기를 누르기 직전의 그 순간입니다.

솔직하게, 이 숫자의 한계. 이 값은 서버 자신이 자기 도메인을 호출해 잰 것이라 네트워크 지연이 거의 없습니다. 실제 방문자는 여기에 통신 거리만큼(집–서버 간 왕복)이 더해집니다. 즉 방문자 체감 TTFB는 이보다 큽니다. 다만 서버가 페이지를 만들어내는 시간 자체가 5배 줄었다는 사실은 변하지 않고, 그 차이는 모든 방문자에게 똑같이 얹힙니다.

어떻게 적용했나 — 설정 3곳

플러그인 설치 화면은 한 번도 열지 않았습니다. 손댄 파일은 딱 두 개, nginx.conf와 사이트 설정 파일입니다.

1. 캐시 저장소 정의 (nginx.conf의 http 블록)

먼저 캐시를 어디에, 얼마나 저장할지 선언합니다. 이름은 DDLABX로 붙였습니다.

fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=DDLABX:100m inactive=60m max_size=256m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

마지막 줄이 핵심입니다. 워드프레스는 요청마다 Set-Cookie 헤더를 붙이는데, 이걸 그대로 두면 “쿠키가 있으니 캐시하지 말라”는 뜻이 되어 캐시가 아예 잡히지 않습니다. 그래서 이 헤더를 의도적으로 무시하게 했습니다.

2. 캐시에서 뺄 요청 지정 (server 블록 상단)

모든 걸 캐시하면 안 됩니다. 로그인한 사용자에게 다른 사람의 로그인 화면이 저장돼 나가면 사고입니다. 관리자·피드·사이트맵도 제외합니다.

set $skip_cache 0;
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|sitemap(_index)?.xml") { set $skip_cache 1; }
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") { set $skip_cache 1; }

sitemap 제외가 중요합니다. 사이트맵이 옛 캐시로 굳어 있으면 크롤러가 새 글을 늦게 발견할 수 있습니다. 캐시 대상에서 빼두면 항상 최신 사이트맵이 나갑니다.

3. PHP 블록에 캐시 적용 + 진단 헤더

마지막으로 PHP 처리 블록 안에 캐시를 켜고, 지금 상태가 MISS인지 HIT인지 눈으로 볼 수 있는 진단 헤더를 답니다.

fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache DDLABX;
fastcgi_cache_valid 200 301 302 60m;
add_header X-FastCGI-Cache $upstream_cache_status;

설정을 저장한 뒤에는 반드시 문법 검사부터 합니다. 통과해야만 반영합니다.

sudo nginx -t
sudo systemctl reload nginx

검증은 같은 페이지를 두 번 호출해 헤더를 보는 것으로 끝납니다. 첫 요청은 MISS(저장 중), 두 번째부터 HIT면 성공입니다.

curl -sI https://ddlabx.com/ | grep -i x-fastcgi-cache

이 방식의 그늘 — 알고 써야 할 3가지

속도를 얻는 대신 감수해야 하는 것들이 있습니다. 캐시는 공짜가 아닙니다.

첫째, 첫 방문자는 여전히 느립니다. 저장본이 없거나 60분이 지나 만료되면 그 사람은 MISS를 겪습니다. 캐시는 “두 번째부터”의 기술입니다.

둘째, 글을 수정해도 방문자는 옛 버전을 볼 수 있습니다. 저장본이 살아 있는 동안(최대 60분)은 수정 전 화면이 나갑니다. 급하면 캐시를 직접 비워야 합니다.

셋째, 글 쓰는 본인은 효과를 체감 못 할 수 있습니다. 관리자로 로그인한 상태에서는 규칙에 따라 BYPASS가 되어 늘 새로 조립된 페이지를 봅니다. 속도 변화를 확인하려면 로그아웃 상태나 curl로 봐야 합니다.

캐시 비우기 1. 수동 — 지금 당장, 플러그인 없이

수정한 글을 즉시 반영하고 싶을 때는 저장 폴더를 통째로 비우면 됩니다. 명령 한 줄이면 끝입니다.

sudo rm -rf /var/cache/nginx/fastcgi/*

비운 직후 첫 방문자는 MISS(새로 조립)를 겪고, 그다음부터 다시 HIT로 쌓입니다. FastCGI 캐시는 저장된 파일 이름이 URL과 1:1로 대응하지 않고 해시로 관리돼서, “이 글 하나만 콕 집어 지우기”는 손으로 하기 번거롭습니다. 그래서 수동으로 할 때는 전체 비우기가 가장 확실하고 빠릅니다. 글을 어쩌다 한 번 고치는 정도라면 이 방법으로 충분합니다.

캐시 비우기 2. 자동 — 글 저장 시 알아서 (Nginx Helper)

문제는 글을 자주 쓸 때입니다. 발행할 때마다 SSH로 들어가 rm을 치는 건 금방 지칩니다. 그런데 여기엔 구조적인 벽이 하나 있습니다. “글이 저장되는 순간”은 워드프레스(PHP) 안에서만 벌어지는 사건이고, Nginx는 그 바깥에 있어서 언제 글이 바뀌었는지 알 방법이 없습니다. 그래서 워드프레스가 “방금 이 글 바뀌었어!” 하고 신호를 보내주는 다리가 필요하고, 그 다리 역할을 Nginx Helper 플러그인이 합니다.

솔직하게. 이 편의 컨셉은 “플러그인 없이”였고, 캐시 자체는 정말 플러그인 없이 구현했습니다. 하지만 정밀한 자동 퍼지만큼은 플러그인 하나가 가장 현실적이고 안전합니다. 이걸 셸 스크립트로 자작하려면 PHP에 캐시 폴더 삭제 권한을 열어줘야 해서 보안이 지저분해집니다. 그래서 여기서는 원칙을 한 번 접고 Nginx Helper를 씁니다. 플러그인은 딱 이 하나뿐입니다.

1) 캐시 폴더 권한 — PHP가 지울 수 있게. 플러그인은 PHP(www-data)로 동작하므로, 캐시 폴더 주인을 www-data로 맞춰 삭제 권한을 줍니다. Nginx도 www-data로 도니 부작용은 없습니다.

sudo chown -R www-data:www-data /var/cache/nginx/fastcgi
sudo chmod -R 755 /var/cache/nginx/fastcgi

2) 캐시 경로를 wp-config.php에 알려주기. 가장 놓치기 쉬운 부분입니다. Nginx Helper의 “로컬 파일 삭제” 방식은 캐시 폴더 경로를 설정 화면에서 입력받지 않고 wp-config.php의 상수로 읽습니다. 이 상수가 없으면 플러그인은 어디를 지울지 몰라 조용히 아무것도 하지 않습니다(에러조차 안 납니다). /* That's all, stop editing! */ 줄 위에 한 줄을 넣습니다.

define( 'RT_WP_NGINX_HELPER_CACHE_PATH', '/var/cache/nginx/fastcgi' );

저장 후 문법 검사는 필수입니다. wp-config.php는 한 글자만 틀려도 사이트가 500 에러를 냅니다(3편에서 겪은 그것).

php -l /var/www/ddlabx/wp-config.php   # No syntax errors 여야 정상

3) 플러그인 설치. 워드프레스 관리자 → 플러그인 → 새로 추가 → “Nginx Helper”(제작: rtCamp) 검색 → 설치 후 활성화.

4) 설정. 설정 → Nginx Helper 로 들어가 아래처럼 맞춥니다. 여기서 Purging Method가 핵심입니다.

항목
Enable Purge (캐시 제거 활성화)체크
Caching Method (캐싱 방법)nginx Fastcgi cache
Purging Method (캐시 제거 방법)Delete local server cache files
Purge Conditions (제거 조건)글/페이지 발행·수정, 홈, 카테고리 체크

표준 Nginx에는 PURGE 요청으로 특정 URL만 지우는 기능이 빌드에 없는 경우가 많습니다. 그래서 플러그인이 캐시 파일을 직접 지우는 Delete local server cache files 방식을 골랐습니다. 1)에서 폴더 권한을, 2)에서 경로 상수를 맞춘 이유가 모두 이 방식 때문입니다.

캐시 키는 절대 바꾸지 마세요. Nginx Helper는 scheme + "GET" + host + 경로로 캐시 위치를 계산합니다. 그래서 STEP 1에서 넣은 fastcgi_cache_key "$scheme$request_method$host$request_uri"를 그대로 둬야 플러그인이 찾는 위치와 실제 캐시 위치가 일치합니다. 특히 $scheme를 빼면 해시가 어긋나 자동 퍼지가 조용히 실패합니다. 이 함정 하나만으로 반나절을 버릴 수 있습니다.

5) 검증 — 반드시 GET으로. 캐시를 데운 뒤, 관리자에서 수정할 글을 열어 “업데이트”를 누르고 상태를 봅니다. 이때 curl -sI(대문자 I = HEAD 요청)를 쓰면 안 됩니다. 플러그인은 GET 요청으로 만들어진 캐시만 지우므로, HEAD로 확인하면 다 고쳐도 영원히 HIT로 보이는 착시가 생깁니다. 방문자와 똑같이 GET으로 헤더를 봐야 합니다.

글="https://ddlabx.com/글-주소/"

# GET으로 두 번 호출 → 두 번째 HIT (-D - 로 헤더만 출력, 본문은 버림)
curl -s -D - -o /dev/null "$글" | grep -i x-fastcgi-cache
curl -s -D - -o /dev/null "$글" | grep -i x-fastcgi-cache   # HIT 확인

# 관리자에서 그 글 '업데이트' 클릭 후 → 다시 GET으로
curl -s -D - -o /dev/null "$글" | grep -i x-fastcgi-cache   # MISS 면 자동 퍼지 성공

업데이트 직후 첫 호출이 MISS로 바뀌면, 이제 발행·수정할 때마다 손대지 않아도 캐시가 알아서 갱신됩니다.

안 될 때는 로그부터. 계속 HIT라면 wp-config.php에 define( 'NGINX_HELPER_LOG', true );를 추가해 로깅을 켜고, 글을 수정한 뒤 /wp-content/uploads/nginx-helper/의 로그를 보세요. 플러그인이 어느 경로를 지우려 했는지 그대로 찍힙니다. 원인의 십중팔구는 2)번 상수 누락, 또는 캐시 키의 $scheme 문제입니다.

어느 쪽을 쓸까. 글을 가끔 고친다 → 수동(1)로 충분합니다. 자주 발행한다 → 자동(2)이 정답입니다. 저는 발행 빈도가 있는 블로그라 2로 두고, 급할 때만 1로 전체를 비웁니다.

다음 편 예고 — 속도 최적화2편 이미지 용량
서버가 페이지를 내주는 시간은 잡았습니다. 하지만 방문자 브라우저에서 실제로 무거운 건 대개 이미지입니다. 다음 편에서는 이미지를 WebP·AVIF로 자동 변환하고 지연 로딩을 걸어, 같은 서버에서 체감 속도를 한 번 더 끌어올립니다. 역시 실측 숫자로 확인합니다.

댓글 남기기