이미지 전송량 31MB → 1.2MB, CDN과 srcset으로 이미지 최적화하기
WiniLogis와 Autowini의 이미지 리소스에 Akamai Image Manager를 적용해, 이미지 포맷 변환과 리사이즈를 CDN에서 처리하도록 변경했습니다. 이전에 React Core 라이브러리를 분리하고 정적 리소스에 CDN 캐시를 적용했던 작업에 이어, 이번에는 이미지 전송량을 줄이는 작업이었습니다.
적용 전 WiniLogis 블로그 목록 페이지를 확인해 보니 PNG 한 장이 1,240kB, 큰 것은 2,326kB였고, 이런 이미지가 20여 장 내려오면서 페이지 한 번 여는 데 이미지만 31MB를 전송하고 있었습니다. 실제 표시되는 카드 크기에 비하면 지나치게 큰 이미지였습니다.

포맷 자동 변환
Image Manager를 적용하면 CDN이 브라우저의 Accept 헤더를 보고 지원하는 최적 포맷으로 변환해 줍니다. 이 동작은 적용만 하면 자동으로 이루어지기 때문에 코드를 바꿀 필요가 없었습니다. URL은 그대로 두고 요청했을 때 위 1,240kB PNG가 35.5kB AVIF로 내려왔습니다.
페이지 전체로 보면 이미지 전송량이 31MB에서 1.2MB로 줄었고, 로딩 완료 시점도 812ms에서 531ms로 짧아졌습니다.

여기까지가 이번 작업에서 가장 큰 절감이었고, 프론트엔드에서 한 일은 없었습니다. 이후 진행한 리사이즈는 여기에 얹는 추가 최적화입니다.
리사이즈 요청과 폭 결정
리사이즈는 포맷 변환과 달리 적용만으로 동작하지 않았습니다. 사용할 폭이 CDN 정책에 미리 등록되어 있어야 해서, 필요한 폭을 정리해 메가존에 요청했습니다. 160, 320, 480, 640, 720 폭이 등록된 뒤부터 해당 폭의 파생본을 받을 수 있었습니다.
그리고 그중 어떤 폭을 요청할지는 프론트엔드가 결정해야 했습니다. 처음에는 간단하게 생각했습니다. 이미 프로젝트에 isMobile(뷰포트 폭이 599px 이하인지 나타내는 값)이 있으니 모바일이면 작은 폭, 데스크톱이면 큰 폭을 쓰면 될 것 같았습니다. 블로그 목록의 카드 이미지가 데스크톱에서 400px로 표시되니 400에 가까운 480을 쓰면 충분하다고 판단했습니다.
그런데 작업 중 Claude Code로부터 srcset과 sizes를 사용해 브라우저가 폭을 고르게 하는 방식을 제안받았습니다. 왜 그게 더 나은지 확인해 보니, isMobile 분기로는 놓치는 부분이 있었습니다. 표시 폭이 같아도 화면의 픽셀 밀도에 따라 필요한 이미지 크기가 달라진다는 점이었고, 여기서 DPR이라는 개념을 처음 알게 됐습니다.
DPR(Device Pixel Ratio)
DPR은 CSS 픽셀 1개를 그리는 데 사용되는 실제 화면 픽셀 수를 뜻합니다. 고밀도 디스플레이는 같은 물리적 크기에 픽셀을 더 촘촘하게 배치하는데, 이때 CSS 1px을 물리 픽셀 1개로 그리면 글자와 UI가 너무 작아지기 때문에 여러 개로 나눠 그립니다.

일반 사무용 모니터는 보통 DPR 1이고, 맥북이나 아이맥은 2, 스마트폰은 2~3입니다. 1.5나 2.4처럼 정수가 아닌 값도 흔하고 대부분 1.0에서 4.0 사이에 분포합니다. 브라우저 콘솔에서 window.devicePixelRatio로 확인할 수 있습니다.
그래서 400px로 표시되는 이미지에 필요한 픽셀은 400개가 아니었습니다. DPR 2 화면에서는 물리 픽셀 800개에 걸쳐 그려지기 때문에, 480px 이미지를 넣으면 부족한 픽셀을 늘려서 채우게 되고 그만큼 흐릿해집니다.
정리하면 적정 이미지 폭은 표시 폭 × DPR로 정해집니다. 기기가 모바일인지 데스크톱인지만으로는 판단할 수 없었습니다. 같은 데스크톱에서도 일반 모니터는 400px면 충분하지만 맥북은 800px이 필요하고, 모바일 뷰포트 375px도 DPR 2면 750px이 필요합니다.
srcset - 이미지 후보 나열
isMobile 같은 분기로는 DPR까지 반영할 수 없기 때문에, 이 계산을 브라우저에 맡기는 방법을 찾았습니다. <img> 태그의 srcset 속성에 같은 이미지의 여러 폭을 나열하면, 브라우저가 자신의 환경에 맞는 것을 고릅니다.
<img
src="blog_480.jpg"
srcset="blog_320.jpg 320w,
blog_480.jpg 480w,
blog_720.jpg 720w"
/>{URL} {폭}w 형태로 나열하며, 여기서 w는 그 파일의 실제 픽셀 폭입니다. 표시 크기가 아니라는 점이 중요합니다.
sizes - 표시 폭 선언
srcset만 넣었을 때는 기대와 다르게 큰 이미지를 받았습니다. 브라우저는 성능을 위해 CSS 계산이 끝나기 전에 이미지 다운로드를 시작하는데, 그 시점에는 이 이미지가 400px 카드에 들어갈지 화면 전체를 채울지 알 수 없습니다.
그래서 sizes로 표시 폭을 미리 알려줘야 합니다. 생략하면 기본값이 100vw이기 때문에, 400px 카드인데도 화면 전체 폭을 기준으로 계산해 불필요하게 큰 파일을 받습니다.
<img
src="blog_480.jpg"
srcset="blog_320.jpg 320w,
blog_480.jpg 480w,
blog_720.jpg 720w"
sizes="(max-width: 599px) 100vw, 400px"
/>
<!-- 모바일(599px 이하)에서는 뷰포트 전체 폭, 그 외에는 400px로 표시됩니다 -->실제 CSS 레이아웃과 값을 맞춰야 하며, 미디어 쿼리 조건은 프로젝트의 브레이크포인트를 그대로 쓰면 됩니다.
브라우저의 폭 선택 과정
sizes에서 표시 폭을 구하고, 자신의 DPR을 곱해 필요한 픽셀을 계산한 뒤, srcset 후보 중 그 값 이상인 가장 작은 폭을 선택합니다.

후보에 필요한 폭 이상이 없으면 가장 큰 폭을 사용합니다. 예를 들어 모바일 뷰포트 375px에서 DPR 2라면 750px이 필요하지만 후보의 상한이 720이므로 720을 받습니다.
적용 결과
블로그 목록뿐 아니라 메인 페이지의 운임 카드에도 같은 방식을 적용했습니다. 카드에 들어가는 국가 배경 이미지는 폭에 따른 용량 차이가 뚜렷해서 효과를 확인하기 좋았습니다. DPR 2 데스크톱에서는 720px(129.7kB)을, DPR 1 모니터에서는 480px(58.8kB)을 받았고, 리사이즈 없이 내려가던 원본은 222.4kB였습니다. 화면에 따라 필요한 만큼만 받게 된 것입니다.

실제로 브라우저가 어떤 폭을 골랐는지는 개발자 도구 Network 탭에서 요청된 이미지 URL로 확인할 수 있습니다.

사용자가 업로드하는 이미지에서는 srcset이 특히 중요했습니다. 리뷰 이미지의 원본 폭을 확인해 보니 607px부터 5712px까지 제각각이었는데, 고정 폭을 쓰면 어떤 이미지에서는 낭비가 되고 어떤 이미지에서는 원본보다 큰 파생본을 받는 경우까지 있었습니다. 폭 선택을 브라우저에 맡기는 편이 안전했습니다.
알게 된 점
- 브라우저는 큰 파일을 이미 받은 뒤 창을 줄여도 작은 파일로 다시 내려받지 않습니다. 폭을 바꿔 확인할 때는 새로고침이 필요합니다.
- CSS background-image에는 srcset을 쓸 수 없습니다. 이 경우에는 표시 폭을 기준으로 고정 폭을 정하거나, 웹과 모바일의 표시 폭 차이가 크면 분기해야 했습니다.
- CDN이 원본보다 큰 폭으로는 확대해 주지 않기 때문에, 원본이 이미 표시 크기보다 작은 이미지에 폭을 지정하는 것은 의미가 없었습니다.
느낀 점
처음에는 isMobile 분기로 해결하려던 문제였는데, 결국 기기를 판별하는 방향이 아니라 표시 폭만 선언하고 판단은 브라우저에 넘기는 방식이 답이었습니다. srcset과 sizes는 2015년경부터 대부분의 브라우저가 지원하던 기능인데 이번에야 제대로 알게 됐고, 앞으로 새로운 기기가 나와도 코드를 고치지 않아도 된다는 점이 특히 좋았습니다.
그리고 DPR을 알고 나서야 예전에 개인 프로젝트에서 겪었던 일이 이해됐습니다. Cloud Flare의 이미지 리사이징을 적용하면서 표시 크기에 가장 잘 맞는 폭을 골라 넣었는데도 화면에서는 이미지가 흐릿하게 보였습니다. 당시에는 원인을 찾지 못하고 넘겼는데, 표시 폭만 보고 화면의 픽셀 밀도를 고려하지 않았기 때문이었습니다.
CDN 설정과 프론트엔드 코드가 맞물려 동작하는 작업이었던 만큼, 이번에도 새로운 개념을 배우고 한 단계 더 성장할 수 있는 계기가 된 것 같습니다.
- #avif
- #devicePixelRatio
- #DPR
- #LCP 개선
- #Sizes
- #srcSet
- #반응형 이미지
- #이미지 CDN
- #이미지 최적화
- #프론트엔드 성능