블로그 이미지 용량 줄이기: PNG, WebP, AVIF 실측 비교로 스크린샷 93% 줄인 방법

휴대폰 스크린샷 3장(647KB)을 44KB로 줄인 과정. 자르기, 크기 맞추기, PNG 색 줄이기, WebP, AVIF를 품질별로 직접 재서 비교하고, picture 태그로 AVIF와 WebP를 함께 쓰는 법까지 정리했다.

스크린샷 3장에 647KB

이 블로그의 서치 콘솔 글에 실제 화면 스크린샷 3장을 넣기로 했다. 휴대폰으로 찍은 원본은 이랬다.

이미지원본 크기 (PNG, 1179×2556)
색인되지 않는 이유 표205KB
크롤링됨 32개 화면207KB
표준 태그 주소 목록235KB
합계647KB

글 하나에 이미지만 650KB 가까이 된다. 휴대폰 데이터로 글을 여는 사람에게는 꽤 무거운 양이다. 최종적으로 이 3장은 44KB가 됐다. 그 과정을 단계별로, 직접 잰 숫자로 정리한다.

크기는 1KB = 1,024바이트로 계산했다. 압축은 파이썬 Pillow 12.3으로 했고, 형식마다 라이브러리가 다르면 결과도 조금씩 달라질 수 있다.


1단계: 필요한 부분만 잘라낸다

가장 먼저, 가장 쉽게 줄이는 방법은 필요 없는 부분을 잘라내는 것이다. 휴대폰 스크린샷에는 상태 표시줄(시간, 배터리), 브라우저 주소창, 계정 아이콘처럼 글과 상관없는 부분이 많다. 이런 부분은 개인 정보이기도 해서 어차피 잘라야 한다.

이미지원본잘라낸 뒤 (PNG)
색인되지 않는 이유 표205KB82KB
크롤링됨 32개 화면207KB91KB
표준 태그 주소 목록235KB152KB

자르기만 해도 절반 가까이 줄었다.


2단계: 화면에 보이는 크기에 맞춘다

이 블로그에서 세로로 긴 스크린샷은 폭 380px로 보여준다. 레티나 화면은 1px을 2개의 화면 점으로 그리니, 선명하게 보이려면 이미지 폭이 그 2배인 760px이면 충분하다. 원본은 폭이 1,133~1,179px이니 더 줄일 수 있다.

그런데 줄였더니 PNG는 오히려 커졌다.

이미지잘라낸 PNG760px로 줄인 PNG
색인되지 않는 이유 표82KB86KB
크롤링됨 32개 화면91KB99KB
표준 태그 주소 목록152KB175KB

크기를 줄일 때 글자 가장자리를 부드럽게 만드느라(LANCZOS 보간) 중간 색이 많이 생기기 때문이다. PNG는 색이 단순할수록 잘 압축되는 형식이라, 픽셀 수가 줄어도 색이 복잡해지면 파일이 커질 수 있다. 그래서 PNG에서는 크기 줄이기만으로는 효과가 없고, 다음 단계가 필요하다.


3단계: 형식과 품질을 직접 비교한다

760px로 줄인 이미지를 여러 방식으로 저장해서 크기를 쟀다.

방식이유 표32개 화면주소 목록
PNG86KB99KB175KB
PNG 64색32KB34KB58KB
PNG 32색28KB28KB50KB
PNG 16색25KB26KB36KB
WebP 무손실38KB49KB69KB
WebP q8522KB23KB43KB
WebP q7518KB18KB35KB
WebP q6016KB16KB32KB
WebP q5015KB15KB30KB
AVIF q6016KB16KB26KB
AVIF q4512KB12KB20KB

표에서 보이는 것:

  • PNG는 색 수를 줄이면 크게 작아진다. 화면 캡처는 쓰는 색이 적어서, 16~64색으로 줄여도 눈으로는 거의 차이가 없다. 86KB가 25KB가 됐다. PNG를 꼭 써야 한다면 이 방법이 가장 효과적이다.
  • WebP 무손실은 생각보다 크다. 색을 줄인 PNG보다도 크다. 화면 캡처에는 무손실보다 손실 압축이 맞다.
  • 같은 품질 숫자라도 형식마다 결과가 다르다. 품질 숫자는 형식마다 기준이 달라서, "q60끼리" 비교하는 건 의미가 없다. 실제로 눈으로 보고 판단해야 한다.
  • AVIF가 가장 작다. q45에서 WebP q60보다 20~40% 더 작다.

눈으로 확인하기

크기만 보고 고르면 안 된다. 글자가 번지면 스크린샷을 넣는 의미가 없다. 같은 부분을 2배로 확대해서 비교했다.

PNG 16색 26.1KB, WebP q50 14.8KB, WebP q60 16.0KB, AVIF q45 12.4KB로 저장한 같은 화면을 2배로 확대해 위아래로 비교한 이미지. 진한 회색 바탕의 날짜 글자와 흰 버튼의 '세부정보 보기' 글자가 보인다
같은 부분을 2배로 확대한 비교. 이 비교 이미지는 압축 흔적이 그대로 보이게 무손실로 저장했다

솔직히 말하면 네 가지 모두 글자는 잘 읽힌다. 차이는 작다. 자세히 보면 WebP q50은 흰 버튼 글자 주변이 아주 약간 뿌옇고, AVIF q45는 가장 작으면서도 깨끗했다. 그래서 AVIF q45를 골랐다.


4단계: AVIF를 쓰되, 못 여는 브라우저는 WebP로

AVIF는 최신 브라우저는 다 지원하지만, Safari는 16(iOS 16)부터 지원한다. 그보다 오래된 기기를 위해 WebP도 같이 둔다. HTML의 <picture> 태그를 쓰면 브라우저가 열 수 있는 첫 번째 형식을 고른다.

<picture>
  <source srcset="/images/posts/shot.avif" type="image/avif" />
  <img src="/images/posts/shot.webp" alt="..." width="380" height="427" loading="lazy" decoding="async" />
</picture>

AVIF를 열 수 있는 브라우저는 <source>의 AVIF를, 못 여는 브라우저는 <img>의 WebP를 받는다. 실제로 개발 서버에서 Chromium으로 열어 보니 3장 모두 AVIF로 받았다.

함정: <source>의 파일이 없으면 깨진다. "AVIF가 없으면 알아서 WebP로 넘어가겠지"라고 생각하기 쉬운데, 그렇지 않다. 브라우저는 <source>의 형식(type)만 보고 고른다. 고른 파일이 404여도 <img>로 넘어가지 않고 깨진 이미지를 보여준다. Chromium에서 AVIF 파일을 일부러 빼고 열어 보니 실제로 깨진 이미지가 나왔다. 그래서 AVIF와 WebP는 항상 짝으로 만들어 둬야 한다.

마크다운에서 이렇게 쓰게 했다

이 블로그는 Nuxt Content로 마크다운 글을 쓴다. 마크다운의 이미지(![설명](주소))를 그리는 컴포넌트를 직접 만들어서, .webp를 쓰면 같은 이름의 .avif를 자동으로 붙이게 했다.

![대체 텍스트](/images/posts/shot.webp "캡션"){width="380" height="427"}
<!-- components/content/ProseImg.vue (요약) -->
<template>
  <figure>
    <picture>
      <source v-if="avifSrc" :srcset="avifSrc" type="image/avif" />
      <img :src="src" :alt="alt" :width="width" :height="height" loading="lazy" decoding="async" />
    </picture>
    <figcaption v-if="title">{{ title }}</figcaption>
  </figure>
</template>

<script setup>
const props = defineProps({ src: String, alt: String, title: String, width: [String, Number], height: [String, Number] });

const avifSrc = computed(() =>
  props.src.endsWith(".webp") && !props.src.endsWith(".lossless.webp")
    ? props.src.replace(/\.webp$/, ".avif")
    : null
);
</script>

위의 비교 이미지처럼 압축 흔적을 그대로 보여줘야 하는 이미지는 무손실 WebP(이름.lossless.webp)로만 두고 AVIF를 찾지 않게 했다.

원래 Nuxt Content는 마크다운 이미지를 @nuxt/image의 NuxtImg로 그린다. 이 블로그는 Vercel에 배포하는데, 그러면 이미지가 Vercel의 이미지 변환을 거쳐서 나간다. 직접 줄인 파일이 그대로 나간다는 보장이 없어서 이 방식을 쓰지 않았다.


5단계: 늦게 불러오고, 자리는 미리 잡는다

용량을 줄이는 것과 함께, 언제 받느냐도 중요하다.

속성하는 일
loading="lazy"화면 근처에 오기 전까지 이미지를 받지 않는다
decoding="async"이미지를 푸는 작업이 글자 그리기를 막지 않게 한다
width, height이미지가 오기 전에 자리를 미리 잡아서, 이미지가 뜰 때 글이 아래로 밀리지 않게 한다

개발 서버에서 확인해 보니, 글을 처음 열었을 때는 첫 번째 이미지 1장만 받고, 나머지 2장은 스크롤해서 가까워졌을 때 받았다.

width/height에는 화면에 보이는 크기를 넣는다. 이미지 파일은 760×853px이지만 380px 폭으로 보이니 width="380" height="427"이다. CSS에서 max-width: 100%; height: auto를 주면 휴대폰처럼 화면이 더 좁을 때도 비율을 지키며 줄어든다.


결과

이미지원본최종 (AVIF q45)줄어든 비율
색인되지 않는 이유 표205KB12KB94%
크롤링됨 32개 화면207KB12KB94%
표준 태그 주소 목록235KB20KB92%
합계647KB44KB93%

AVIF를 못 여는 브라우저가 받는 WebP q60도 합계 64KB(90% 감소)다.

내가 쓴 변환 코드

파이썬 Pillow로 자르고, 줄이고, 두 형식으로 저장했다.

from PIL import Image

im = Image.open("screenshot.png").convert("RGB")
im = im.crop((23, 913, 1156, 2184))             # 1단계: 필요한 부분만 (왼쪽, 위, 오른쪽, 아래)
w = 760                                          # 2단계: 표시 폭 380px의 2배
im = im.resize((w, round(im.height * w / im.width)), Image.LANCZOS)

im.save("shot.avif", "AVIF", quality=45)         # 3~4단계: 기본
im.save("shot.webp", "WEBP", quality=60, method=6)  # AVIF를 못 여는 브라우저용

정리

단계효과
필요한 부분만 자르기개인 정보도 지우고, 용량도 절반 가까이 줄인다
표시 크기의 2배로 줄이기레티나에서도 선명하다. 단, PNG는 이것만으로는 오히려 커질 수 있다
형식과 품질 직접 비교숫자만 보지 말고 확대해서 글자가 번지는지 본다
AVIF + WebP를 <picture>로가장 작은 형식을 쓰면서 오래된 브라우저도 챙긴다. 두 파일은 항상 짝으로
loading="lazy", width/height필요할 때 받고, 글이 밀리지 않게 한다

이미지 압축 도구는 많지만, 내 이미지에서 무엇이 가장 작은지는 직접 재 봐야 안다. 이번처럼 화면 캡처는 "PNG 색 줄이기"가 생각보다 강하고, "WebP 무손실"이 생각보다 약했다. 사진이라면 결과가 또 다를 것이다.

참고 자료