WebP가 JPEG보다 커질 때: 사진 종류별 WebP·AVIF 실측
인쇄기 롤러 견적 사이트를 만들면서 화면에 나가는 사진을 전부 WebP로 정했습니다. 제품 사진 네 장으로 재 보고 내린 결론이었습니다. 그 뒤에 설계도 사진 세 장이 들어왔고, 같은 스크립트에 그대로 태웠습니다. 이 글을 쓰려고 다시 재 보니 그중 두 장은 WebP가 JPEG보다 컸습니다.
원본은 assets, 화면에 나가는 파일은 public
원본 사진은 assets/에 커밋하고, 화면에 나가는 파일은 빌드 때 public/에 만듭니다. public/은 커밋하지 않습니다. 둘 다 커밋해 두면 원본을 바꾸고 굽기를 잊은 날 두 벌이 조용히 갈라집니다.
변환은 sharp로 짠 스크립트 하나가 합니다. npm run dev와 npm run build 앞에 predev·prebuild로 물려 있고, 결과 파일이 원본보다 새것이면 건너뜁니다. dev 서버를 켤 때마다 다시 굽지 않게 하려는 것입니다.
const JOBS = [
{ from: "assets/rollers", to: "public/rollers", ext: ".webp",
encode: (img) => img.resize({ width: 768, withoutEnlargement: true }).webp({ quality: 75 }) },
{ from: "assets/brand", to: "public/brand", ext: ".png",
encode: (img) => img.png({ palette: true, colors: 64, compressionLevel: 9 }) },
{ from: "assets/blueprints", to: "public/blueprints", ext: ".webp",
encode: (img) => img.rotate().resize({ width: 1600, withoutEnlargement: true }).webp({ quality: 80 }) },
];Next.js의 next/image는 쓰지 않았습니다. next/image는 요청이 들어올 때 서버가 크기와 형식을 바꿔 캐시해 두는 방식입니다. 이 사이트의 사진은 요청마다 달라질 것이 없어서, 빌드 때 한 번 줄여 두면 런타임에 할 일이 남지 않습니다.
롤러 사진 네 장에서 WebP를 고른 근거
제품 카드의 폭이 380px 안팎이라 사진은 폭 768px로 줄였습니다. 2배 밀도 화면에서도 선명한 크기입니다. 네 장을 형식별로 구우면 이렇습니다.
| 형식 (설정) | 네 장 합계 | 인코딩 시간 (네 장) |
|---|---|---|
| 원본 JPEG | 164.2KB | |
| JPEG (mozjpeg q75) | 52.4KB | |
| WebP q75 | 31.6KB | 61ms |
| AVIF q50 | 20.3KB | 225ms |
AVIF가 가장 작았습니다. 그래도 네 장을 합쳐 11KB 차이였고 인코딩은 3.7배 걸렸습니다. 카탈로그 첫 화면의 이미지 전송량이 로고까지 합쳐 37.2KB라, 11KB를 더 줄이자고 형식을 올릴 이유를 찾지 못했습니다. 브라우저 지원도 WebP가 AVIF보다 몇 해 앞서 있습니다. WebP로 정했습니다.
카드의 <img>에는 width={768} height={512}를 적어 두었습니다. 사진이 늦게 도착해도 브라우저가 자리를 먼저 잡아 두니 카드 아래가 밀리지 않습니다. 화면 밖의 카드는 loading="lazy"로 스크롤할 때 받습니다.
글자 테두리 때문에 PNG로 남긴 로고
로고는 159×35 래스터 한 장뿐입니다. 손실 압축에서는 작은 글자의 테두리가 뭉개졌습니다. 그래서 로고만 무손실 PNG로 두고 색을 64개로 줄였습니다(팔레트 PNG). 16.3KB가 5.6KB가 됐습니다. 색이 몇 개 안 되는 그림은 색 수를 줄여도 눈에 띄는 것이 없고, 줄인 만큼 무손실 압축이 잘 먹힙니다.
레티나 화면에서는 조금 흐립니다. 원본이 그 크기뿐이라 벡터 원본을 받으면 SVG로 바꿀 생각입니다.
설계도 사진에서 뒤집힌 WebP와 JPEG의 순서
설계도는 거래처가 처음 건넨 자료 세 장입니다. 한 장은 종이에 손으로 그린 옵션 스케치이고, 두 장은 엑셀 규격표가 떠 있는 모니터를 휴대폰으로 찍은 사진입니다. 글씨가 읽혀야 해서 폭을 1600px로 넉넉히 두고 WebP q80으로 구웠습니다. 롤러 사진에서 WebP가 이겼으니 여기서도 그럴 거라 여겼고, 따로 재지 않았습니다.
| 사진 | 원본 | JPEG (mozjpeg q80) | WebP q80 | AVIF q50 |
|---|---|---|---|---|
| 손그림 스케치 (세로) | 428.3KB | 119.6KB | 65.4KB | 46.3KB |
| 모니터 속 엑셀 표 ① | 2,330.3KB | 379.4KB | 459.4KB | 165.0KB |
| 모니터 속 엑셀 표 ② | 2,087.8KB | 353.8KB | 424.4KB | 146.0KB |
| 합계 | 4,846.4KB | 852.9KB | 949.2KB | 357.4KB |
손그림은 롤러 사진과 같은 순서가 나왔습니다. 모니터를 찍은 두 장은 WebP가 JPEG보다 각각 80KB, 71KB 컸습니다. 세 장을 합치면 WebP를 쓰는 바람에 JPEG로 냈을 때보다 96KB를 더 보내고 있었습니다. AVIF와의 차이는 11KB가 아니라 592KB였습니다.
품질 숫자는 인코더마다 척도가 달라서 같은 숫자가 같은 화질을 뜻하지 않습니다. 위 표는 각 설정에서 나온 크기이고 화질을 맞춘 비교가 아닙니다. 그래서 화질은 눈으로 따로 봤습니다.
WebP가 용량을 쓴 곳, 모니터 잔무늬
엑셀 표 사진의 같은 조각을 세 형식으로 잘라 1:1로 나란히 놓았습니다. 숫자와 한글은 셋 다 또렷이 읽혔습니다. 차이는 배경에 있었습니다. 모니터를 찍은 사진에는 화면 픽셀이 만든 잔무늬(모아레)가 깔려 있는데, AVIF는 이것을 조금 뭉개 평평하게 만들었고 WebP와 JPEG는 그대로 살렸습니다.
WebP가 여기서 JPEG에 진 이유가 이 무늬라고 봅니다. 읽어야 할 정보와 상관없는 잔무늬를 충실히 옮기느라 용량을 썼다는 뜻입니다. 확인한 것은 아닙니다. 인코더 설정을 바꿔 가며 원인을 좁혀 보지는 않았습니다.
빌드는 초록불인데 이미지가 404인 구조
이미지를 빌드 때 굽는 방식에는 함정이 하나 따라옵니다. Docker 이미지를 npx next build로 지으면 npm의 prebuild 훅이 돌지 않아서 public/에 사진이 생기지 않습니다. 그리고 Next.js의 standalone 산출물은 public/을 가져가지 않아서 러너 단계로 따로 복사해야 합니다. 둘 중 하나만 빠져도 빌드는 성공하고 배포도 초록불인데 사진과 로고가 전부 404입니다.
그래서 Dockerfile의 해당 줄마다 왜 npm run build인지, 왜 public/을 복사하는지 주석을 붙여 두었습니다. 누가 npx next build로 줄이려는 순간 그 주석을 먼저 읽게 됩니다.
다시 골라야 하는 설계도 사진의 형식
후보는 둘입니다. 설계도만 AVIF로 옮기거나, 사진마다 형식을 달리해 모니터 사진은 JPEG로 내는 것입니다. 저는 AVIF 쪽으로 기울어 있습니다. 설계도는 빌드 때 한 번 굽고 끝이라 세 장에 1.4초 걸리는 인코딩은 비용이라 할 것이 못 되고, 592KB는 11KB와 달리 휴대폰 회선에서 체감되는 크기입니다.
설계도 페이지의 <img>에는 width와 height도 빠져 있습니다. 롤러 카드에는 적어 두고 여기서는 잊었습니다. 사진이 늦게 오면 그 아래 내용이 한 번 밀려 내려갑니다.
형식을 한 번 정하면 그 뒤에 들어오는 사진에도 그 결론이 조용히 따라붙습니다. 결론을 낸 표본이 제품 사진 네 장이었다는 사실은 설정 파일에 남지 않았습니다.
함께 읽기
- 웹 퍼블리셔와 프론트엔드 개발자 차이: 산출물이 갈리는 지점견적서에 이런 두 줄이 나란히 놓이는 경우가 있습니다.
- GA4 쿠키 없이 쓰기: client_id를 직접 넘기는 구성방문 분석은 하고 싶은데 쿠키 동의 팝업은 만들기 싫었습니다. 배너를 하나 붙이면 화면에 층이 하나 더 생기고, 동의 상태를 저장하고 갱신하고 철회까지 받는 코드가 따라옵니다. 방문자가 하루 수십 명인 회사 사이트에 그만한 장치를 두는 것은 과합니다.
- Next.js 웹앱을 Electron 데스크톱 앱으로 만들기 전에 따져볼 것사내 웹 도구를 쓰다 보면 어느 순간 "이거 그냥 데스크톱 앱으로 만들면 안 되나"라는 말이 나옵니다.
- Leaflet 지도가 느려지는 3가지 원인: React key, SVG 렌더러, 가드 위치지도 위에 H3 육각 격자를 얹은 화면을 만들었습니다. 셀을 누르면 인접한 여섯 셀이 함께 칠해지는, 영업 구역을 나눠 보는 화면입니다.
- 데스크톱 AI 화면을 모바일에서 과감히 제거한 이유데스크톱에서 잘 작동하는 AI 인터페이스를 모바일 화면에 그대로 줄이면 기능은 남지만 경험은 쉽게 무너집니다. 사이드바, 대화, 참고 문헌을 동시에 보여주는 3단 구조는 넓은 화면에서는 강력하지만 작은 화면에서는 탐색과 스크롤이 서로 경쟁합니다.