모던 웹·앱에서 이미지를 다룰 때, 원본 이미지를 그대로 내려받는 경우는 거의 없다. 대신 이미지 CDN 또는 이미지 변환 서버(image transformation server)에 쿼리 파라미터를 붙여서 "내가 필요한 크기·품질·포맷으로 변환해서 내려달라"고 요청한다. 이 방식을 on-the-fly image resizing 또는 dynamic image optimization이라 부른다.
대표적인 서비스로 Cloudinary, imgix, Thumbor, Cloudflare Images, Next.js Image Optimization, ImageKit 등이 있다. 파라미터 이름은 서비스마다 조금씩 다르지만 개념은 동일하다.
https://cdn.example.com/photo.jpg?w=300&q=85&f=webp
└──────┬──────┘
변환 요청 파라미터
서버는 이 요청을 받으면 원본을 변환한 결과를 생성하고, 그 결과를 캐시에 저장한 뒤 응답한다. 같은 파라미터로 다시 요청이 오면 캐시된 결과를 즉시 반환하므로 변환 비용은 최초 1회만 발생한다.
w / h — 너비·높이 (width / height)출력 이미지의 크기를 픽셀 단위로 지정한다. 가장 핵심적인 파라미터다.
?w=300 // 너비 300px로 리사이즈 (높이는 비율 유지)
?w=300&h=200 // 300×200으로 리사이즈
원본이 4000×3000인 이미지를 화면에서는 300px로만 보여준다면, 원본을 그대로 받는 것은 거대한 낭비다. w=300으로 요청하면 서버가 300px짜리로 줄여서 내려주므로 전송량이 수십 배 줄어든다.
q — 품질 (quality)JPEG·WebP 같은 손실 압축 포맷의 압축 강도를 지정한다. 보통 1~100 범위이며, 값이 낮을수록 용량은 작아지지만 화질이 떨어진다.
?q=85 // 품질 85 (용량/화질 균형점으로 널리 쓰임)
대체로 75~85 구간이 사람 눈에 화질 저하가 거의 느껴지지 않으면서 용량을 크게 줄이는 sweet spot이다. 100은 거의 무손실에 가깝지만 용량 대비 이득이 작아 실무에서는 잘 쓰지 않는다.
f / fm / format — 출력 포맷 (format)이미지를 어떤 포맷으로 변환할지 지정한다.
?f=webp // WebP로 변환
?f=avif // AVIF로 변환
?f=auto // 클라이언트가 지원하는 최적 포맷 자동 선택
포맷별 특징은 아래와 같다.
| 포맷 | 압축률 | 투명도 | 호환성 | 비고 |
|---|---|---|---|---|
| JPEG | 보통 | ✗ | 최고 | 사진에 적합, 오래된 표준 |
| PNG | 낮음(무손실) | ✓ | 최고 | 로고·아이콘·투명 이미지 |
| WebP | 높음 | ✓ | 높음 | JPEG 대비 25~35% 용량 절감 |
| AVIF | 매우 높음 | ✓ | 중간 | WebP보다 더 작지만 디코딩 비용 큼 |
동일 화질 기준으로 WebP는 JPEG보다 보통 25~35% 작고, AVIF는 그보다 더 작다. 다만 AVIF는 디코딩에 CPU를 더 쓰므로 저사양 기기에서는 렌더링이 느려질 수 있어 트레이드오프를 고려해야 한다.
dpr — 디바이스 픽셀 비율 (device pixel ratio)고해상도 디스플레이를 위한 배율이다. 일부 CDN은 w와 별개로 dpr를 받아 내부적으로 곱해준다.
?w=100&dpr=3 // 실제로는 300px짜리를 생성
이 파라미터를 지원하지 않는 서버라면, 클라이언트에서 직접 w에 픽셀 비율을 곱해 보내야 한다(아래 React Native 섹션 참고).
fit / c — 크롭·맞춤 모드 (fit / crop)지정한 박스에 이미지를 어떻게 맞출지 결정한다.
?w=300&h=300&fit=cover // 박스를 꽉 채우고 넘치는 부분은 잘라냄
?w=300&h=300&fit=contain // 비율 유지하며 박스 안에 모두 담음(여백 생길 수 있음)
cover는 썸네일·프로필 이미지처럼 정사각형 박스를 빈틈없이 채워야 할 때, contain은 이미지 전체가 잘리지 않고 보여야 할 때 쓴다.
React Native에서는 PixelRatio를 통해 기기의 픽셀 밀도를 얻어 w 파라미터에 반영하는 패턴이 핵심이다. 레이아웃 단위(dp)와 실제 물리 픽셀(px)이 다르기 때문이다.
import { PixelRatio } from 'react-native';
export const resizeImageUrl = (url: string, width: string): string => {
if (!url) return '';
const newWidth = width * PixelRatio.get();
const quality = 85;
const separator = url.includes('?') ? '&' : '?';
return url + `${separator}w=${Math.floor(newWidth)}&q=${quality}&f=webp`;
};
핵심은 width * PixelRatio.get()이다. PixelRatio.get()은 기기의 DPR을 반환한다(@2x 기기면 2, @3x 기기면 3). 화면에 100dp로 그릴 이미지라도 @3x 기기에서는 300px짜리 이미지를 받아와야 선명하게 보인다. 이 보정을 하지 않으면 고해상도 화면에서 이미지가 뿌옇게 보이고, 반대로 항상 최대 배율로 받으면 저해상도 기기에서 불필요하게 큰 이미지를 받게 된다.
separator 처리는 URL에 이미 쿼리스트링이 있는지에 따라 ?와 &를 구분해 붙여 URL이 깨지지 않게 하는 안전장치다.
PixelRatio.get()을 그대로 곱하면 @3x 기기에서 넓은 이미지를 요청할 때 요청 너비가 과하게 커질 수 있다. 다음과 같은 방식으로 다듬을 수 있다.
// 방법 1: RN이 제공하는 헬퍼 사용 (반올림 처리까지 해줌)
const newWidth = PixelRatio.getPixelSizeForLayoutSize(width);
// 방법 2: DPR에 상한선을 둠 (예: 최대 2배까지만)
const dpr = Math.min(PixelRatio.get(), 2);
const newWidth = width * dpr;
DPR에 상한을 두는 이유는, @3x와 @2x의 실질적 화질 차이가 사람 눈에 잘 구분되지 않는 경우가 많은 반면 용량 차이(3²:2² = 2.25배)는 크기 때문이다. 화질과 용량의 균형을 위해 실무에서 자주 쓰는 기법이다.
가장 직접적인 효과다. 4000×3000(약 5MB) 원본을 300px WebP(수십 KB)로 변환하면 전송량이 수십 배에서 100배 이상 줄어든다. 모바일 데이터 환경에서 체감 로딩 속도가 크게 개선되고, CDN 전송 비용도 절감된다.
이 효과는 특히 React Native에서 중요하다. 이미지는 디코딩되면 압축이 풀린 비트맵 상태로 메모리에 올라간다. 비트맵 메모리는 대략 너비 × 높이 × 4바이트(RGBA)로 계산된다.
긴 리스트에서 원본 이미지를 그대로 쓰면 메모리가 폭증해 OOM(Out Of Memory) 크래시로 이어지기 쉽다. 표시 크기에 맞춰 리사이즈된 이미지를 받으면 이 문제를 근본적으로 줄일 수 있다.
디코딩해야 할 픽셀 수가 줄어들면 디코딩 시간이 짧아지고, GPU에 올리는 텍스처 크기도 작아진다. 그 결과 이미지가 화면에 표시되기까지의 지연이 줄고, 리스트 스크롤 시 프레임 드랍이 완화된다.
f=webp 또는 f=avif로 변환하면 같은 화질에서 용량을 추가로 줄인다. WebP는 JPEG 대비 평균 25~35% 작으므로, 리사이즈 효과와 곱해져 전체 절감 폭이 더 커진다.
변환 결과는 파라미터 조합을 키로 캐싱된다. 즉 ?w=300&q=85&f=webp로 들어온 요청은 최초 1회만 변환되고 이후에는 엣지 캐시에서 즉시 응답된다. 사용하는 파라미터 조합의 가짓수를 일정하게 유지(예: DPR 상한, 정해진 너비 단계 사용)하면 캐시 적중률이 높아져 변환 부하와 응답 지연이 함께 줄어든다.