7편에서 FastCGI 캐시로 서버가 페이지를 내주는 시간을 잡았습니다. 하지만 방문자 브라우저가 실제로 낑낑대며 내려받는 건 대개 글자가 아니라 이미지입니다. 이번 편에서는 같은 서버에서 이미지 용량 자체를 줄입니다. 방법은 1편과 같습니다. 플러그인은 설치하지 않고, 심지어 글의 HTML도 한 글자 고치지 않습니다. 서버가 방문자의 브라우저를 보고 알아서 가벼운 버전을 골라 내보내게 만듭니다.
결론부터. 원본 이미지는 그대로 두고 옆에 WebP를 만들어 두자, 사이트 전체 이미지 용량이 16.99MB → 4.07MB(76% 감소, 4.2배)로 줄었습니다. 실제 게시 이미지 한 장 기준으로는 브라우저에 전송되는 용량이 56.7KB → 18.8KB(67% 감소)였고요. 플러그인 0개, HTML 수정 0줄, 원본 이미지는 하나도 삭제하지 않았습니다.
페이지를 무겁게 하는 진짜 범인
7편의 TTFB(서버가 첫 바이트를 내주기까지의 시간)와 이번 편은 다른 문제를 다룹니다. 캐시가 “서버가 얼마나 빨리 반응을 시작하느냐”였다면, 이미지는 “그 뒤로 방문자가 얼마나 많은 데이터를 내려받아야 하느냐”입니다. 글 한 편에 캡처와 다이어그램이 몇 장만 들어가도, 텍스트보다 이미지가 페이지 무게의 대부분을 차지하는 경우가 흔합니다.
모바일에서 이 무게는 곧 로딩 대기와 데이터 소모로 이어지고, 구글이 페이지 속도(Core Web Vitals)의 LCP를 평가할 때도 가장 크고 늦게 뜨는 요소가 보통 이미지입니다. 즉 이미지를 줄이는 건 속도와 검색 노출을 동시에 건드리는 작업입니다.
WebP·AVIF가 기존 이미지와 뭐가 다른가
WebP와 AVIF는 같은 화질을 훨씬 적은 용량으로 담는 최신 이미지 포맷입니다. 특징을 정리하면 이렇습니다.
| 포맷 | 압축 | 브라우저 지원 | 이번 편에서 |
|---|---|---|---|
| JPG / PNG | 기준 | 모든 곳 | 원본으로 보관 |
| WebP | 강함 | 사실상 전부(수년째 표준) | 실제 서빙에 사용 |
| AVIF | 가장 강함 | 최신 브라우저 대부분 | 비교용 측정만 |
핵심 전략은 “원본을 버리지 않는다”입니다. WebP를 지원하는 브라우저에는 WebP를, 지원하지 않는 구형 환경에는 원본 JPG/PNG를 그대로 내보냅니다. 방문자는 아무것도 몰라도 되고, 저는 어느 쪽에서도 이미지가 깨지지 않습니다.
실측 1. 이미지 한 장 — 999KB가 20KB로
측정 환경
| 항목 | 값 |
|---|---|
| 서버 | Vultr 서울 · Ubuntu 24.04 · 1 vCPU · RAM 2GB |
| WebP 인코더 | cwebp 1.3.2 (품질 q80) |
| AVIF 인코더 | avifenc 1.0.4 (품질 q60) |
| 샘플 이미지 | 1777 × 885 PNG 한 장 |
가장 큰 원본 이미지 한 장을 골라 WebP와 AVIF로 각각 변환하고 용량을 나란히 쟀습니다.
| 포맷 | 용량 | 원본 대비 |
|---|---|---|
| 원본 PNG | 999.1 KB (1,023,079 B) | 기준 |
| WebP (q80) | 28.7 KB (29,402 B) | 97.1% 감소 |
| AVIF (q60) | 20.4 KB (20,900 B) | 98.0% 감소 |
이 숫자를 그대로 믿으면 안 되는 이유
이 샘플은 PNG였습니다. PNG는 스크린샷·다이어그램처럼 색이 단순한 이미지에 쓰이고, 이런 이미지는 WebP·AVIF로 바꿀 때 용량이 극적으로 줄어듭니다(그래서 97%가 나온 겁니다). 반대로 색이 복잡한 사진 JPG는 이미 JPG 자체가 압축이라, 감량 폭이 이보다 훨씬 작습니다. 그러니 “97% 줄어든다”를 사이트 전체에 그대로 대입하면 안 됩니다. 아래 전체 통계가 더 현실적인 숫자입니다.
실측 2. 사이트 전체 — 349장, 17MB → 4MB
블로그의 업로드 폴더 전체를 WebP로 변환했습니다. 원본은 349장 그대로 두고, 옆에 WebP 349장을 새로 만든 결과입니다.
| 구분 | 파일 수 | 합계 용량 |
|---|---|---|
| 원본 (JPG·PNG) | 349장 | 16.99 MB |
| 생성한 WebP | 349장 | 4.07 MB |
| 절감 | — | 12.92 MB (76% ↓ · 4.2배) |
스크린샷이 많은 테크 블로그라 감량 폭이 큰 편이지만, 사진 위주 블로그라도 절반 안팎은 무난히 줄어듭니다. 저장 공간 부담도 확인했습니다. 디스크는 60GB 중 12GB만 쓰고 46GB가 비어 있어서, WebP 4MB가 추가로 쌓이는 건 사실상 없는 셈 칩니다. 원본을 버리지 않고도 여유가 넉넉합니다.
HTML은 건드리지 않는다 — Nginx가 알아서 고른다
여기가 이번 편의 핵심입니다. 보통은 이미지 태그를 <picture>로 바꿔야 한다고들 하는데, 저는 글을 하나도 수정하지 않았습니다. 대신 Nginx가 브라우저의 요청 헤더를 보고 파일을 바꿔치기하게 했습니다.
1. 원본 옆에 WebP 만들기 (일괄, 중복 스킵)
cd /var/www/ddlabx/wp-content/uploads
sudo find . -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) \
-exec sh -c 'for f; do [ -f "$f.webp" ] || cwebp -quiet -q 80 "$f" -o "$f.webp"; done' _ {} +
sudo find . -iname '*.webp' -exec chown www-data:www-data {} +
이미 변환한 파일은 건너뛰기 때문에 몇 번을 돌려도 안전합니다. 뒤에서 이 특성을 그대로 자동화에 씁니다.
2. 브라우저에 맞춰 파일을 고르는 규칙
nginx.conf의 http 블록에 map 하나를 추가합니다. “브라우저가 WebP를 받겠다고 하면 접미사 .webp를, 아니면 빈 값을” 담는 변수입니다.
map $http_accept $webp_suffix {
default "";
"~*image/webp" ".webp";
}
그리고 사이트 설정의 443 서버 블록 안에 이미지용 규칙을 넣습니다.
location ~* ^/wp-content/uploads/.+\.(jpe?g|png)$ {
add_header Vary Accept;
try_files $uri$webp_suffix $uri =404;
expires 30d;
access_log off;
}
try_files $uri$webp_suffix $uri가 전부입니다. WebP 지원 브라우저면 사진.png.webp를 먼저 찾고, 없거나 미지원이면 원본 사진.png로 넘어갑니다. Vary Accept는 중간 캐시가 WebP와 원본을 헷갈려 엉뚱한 방문자에게 잘못된 버전을 주지 않게 막는 안전장치입니다.
3. 진짜로 바꿔치기되는지 검증
같은 이미지를, 브라우저인 척 두 가지 헤더로 요청해 실제 전송 용량을 쟀습니다.
| 요청(브라우저 흉내) | 실제 응답 포맷 | 전송 용량 |
|---|---|---|
| WebP 지원 | image/webp | 18.8 KB (19,244 B) |
| WebP 미지원 | image/png | 56.7 KB (58,093 B) |
같은 주소인데 응답이 다릅니다. 지원 브라우저에는 WebP가 나가 67%(3배) 가벼워졌고, 미지원 환경에는 원본 PNG가 그대로 나갑니다. 이게 “HTML을 안 고쳤다”의 증거입니다.
지연 로딩은 이미 되어 있다
화면 아래쪽 이미지를 스크롤이 닿을 때 불러오는 지연 로딩(lazy loading)은 워드프레스가 5.5부터 자동으로 붙입니다. 홈페이지에서 확인해 보니 loading="lazy"가 7개 걸려 있었습니다. 손댈 것 없이 이미 작동 중이라, 이번 편에서 추가 작업은 없습니다.
솔직한 한계 3가지
첫째, AVIF는 더 작지만 이번엔 안 썼습니다. 측정에서 AVIF는 WebP보다 29% 더 작았지만, 인코딩이 훨씬 느립니다. 1 vCPU 서버에서 수백 장을 AVIF까지 만들면 CPU와 저장공간을 두 배로 쓰게 됩니다. WebP만으로도 이미 4배를 줄였으니, AVIF는 효과 대비 비용이 큰 다음 단계로 남겨두는 게 합리적입니다.
둘째, 새로 올리는 이미지는 자동으로 WebP가 되지 않습니다. 위 변환은 “지금 있는 파일”에 대한 일회성 작업입니다. 앞으로 글을 쓰며 올리는 이미지는 WebP가 없으니, 그대로 두면 원본만 나갑니다. 아래 자동화가 필요한 이유입니다.
셋째, 감량 폭은 이미지 종류에 크게 좌우됩니다. 스크린샷 많은 블로그라 76%가 나왔지만, 사진 위주라면 절반 안팎이 현실적입니다. 그래도 “안 하는 것보다 항상 낫다”는 방향은 같습니다.
새 이미지 자동 변환 (cron)
일회성 작업을 매일 새벽에 자동으로 돌리게 걸어둡니다. 이미 WebP가 있는 파일은 건너뛰므로, 새로 올린 이미지만 변환됩니다.
# crontab -e 에 추가 (매일 새벽 4시)
0 4 * * * find /var/www/ddlabx/wp-content/uploads -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) -exec sh -c 'for f; do [ -f "$f.webp" ] || { cwebp -quiet -q 80 "$f" -o "$f.webp"; chown www-data:www-data "$f.webp"; }; done' _ {} +
글을 발행하고 최대 하루 뒤부터 WebP가 적용됩니다. 발행 직후 바로 반영하고 싶으면 위 find 명령을 수동으로 한 번 실행하면 됩니다.
다음 편 예고 — 속도 최적화 튜닝
서버가 내주는 시간(7편)과 방문자가 받는 무게(8편)를 잡았습니다. 다음 편은 서버 안쪽입니다. PHP 8.3의 OPcache, PHP-FPM 워커 수, MariaDB 버퍼를 1 vCPU·2GB라는 작은 서버에 맞게 손봐서, 캐시가 빗나가는 순간(MISS)의 속도까지 끌어올립니다. 역시 실측으로 확인합니다.