브라우저 이미지 압축: 업로드 전에 160MB를 2.49MB로 줄였다
이미지 네 장의 160.04MB를 브라우저에서 2.49MB로 줄이고, 픽셀 축소·WebP 변환·원본 보존 여부를 결정한 과정.
이 글의 목차브라우저 압축이 해결하는 문제는 업로드 전송량이다8
이미지 업로드 기능을 작업하면서 대용량 샘플 네 장의 크기를 더해 봤다.
합계가 160.04MB였다.
저장 버튼이 동작하는지만 볼 때는 지나치기 쉬운 숫자였지만, 사용자가 이 용량을 매번 서버로 보내야 한다고 생각하니 이야기가 달라졌다.
서버에서 원본을 받은 뒤 이미지를 줄이는 방법도 있다.
구현 환경을 통제하기는 쉽지만, 사용자는 이미 160MB를 전송한 뒤다.
큰 이삿짐을 창고까지 옮겨 놓고 나서 포장을 줄이는 것과 같았다.
이번에는 저장 공간보다 서버까지 이동하는 바이트를 먼저 줄이고 싶었다.
샘플 네 장의 결과는 160.04MB에서 2.49MB였다. 긴 변을 최대 1000px로 줄이고 WebP 품질 0.9로 다시 만들자 전송 대상이 약 98.44% 줄었다.
브라우저 압축이 해결하는 문제는 업로드 전송량이다
이미지 최적화에는 서로 다른 세 문제가 섞여 있다.
저장 공간을 줄이는 일, 웹 페이지에서 내려받는 이미지를 작게 만드는 일, 사용자가 올리는 원본을 서버에 보내기 전에 줄이는 일이다.
이번 작업은 세 번째에 해당한다.
중고 거래 서비스에서 상품 사진 열 장을 올리거나, 현장 점검 앱에서 고해상도 사진을 여러 장 첨부하는 상황을 생각해 볼 수 있다.
서버 압축만 사용하면 사진을 받은 뒤의 저장 비용은 줄지만 사용자의 느린 모바일 업로드는 이미 끝난 다음이다.
브라우저 압축은 네트워크에 싣기 전에 화면용 복사본을 만들어 이 구간을 줄인다.
반대로 신분증 원본, 의료 영상, 인쇄용 사진처럼 픽셀과 메타데이터가 증거가 되는 파일은 같은 방식으로 덮어쓰면 안 된다.
원본을 별도 경로로 보존하고 화면 미리보기만 축소하거나, 규정상 클라이언트 변환 자체를 허용하지 않는 편이 맞을 수 있다.
| 목적 | 권장 경로 |
|---|---|
| 프로필·게시글 첨부 사진 | 브라우저에서 화면 크기로 축소한 뒤 업로드 |
| 원본 보관과 빠른 미리보기가 모두 필요 | 원본과 축소본을 별도 객체로 업로드 |
| 문서 증빙·보정·인쇄 | 원본을 보존하고 서버에서 파생 이미지 생성 |
| 저사양 기기에서 수십 장 처리 | 작업 수를 제한하거나 서버 처리로 폴백 |
품질 값을 내리기 전에 픽셀 수부터 줄였다
WebP는 손실 압축과 무손실 압축, 투명 배경을 지원하는 이미지 형식이다.
손실 압축은 사람이 덜 알아차리는 일부 정보를 버려 파일을 줄이는 방식이고, 무손실 압축은 원래 픽셀을 복원할 수 있게 저장하는 방식이다.
이 작업의 canvas.toBlob()에는 손실 압축에 사용할 품질 힌트를 전달했다.
처음부터 품질 값만 낮추기보다 화면에 필요하지 않은 픽셀부터 덜어 냈다.
해상도는 사진을 이루는 작은 색 점이 가로와 세로에 각각 몇 개 놓였는지를 뜻한다.
스마트폰 사진은 실제 화면에서 쓰는 크기보다 해상도가 훨씬 큰 경우가 많다.
원본 비율은 유지하고, 가로와 세로 중 긴 쪽이 정한 상한을 넘지 않도록 하나의 축소 비율을 계산했다.
원본보다 작은 사진을 도리어 확대하지 않기 위해 비율은 1을 넘지 않게 했다.
압축 파일의 크기와 브라우저 메모리 사용량은 같지 않다.
파일은 화면에 그리기 전에 픽셀로 풀린다.
일반적인 RGBA는 빨강·초록·파랑·투명도 네 채널을 픽셀마다 저장하므로, 픽셀 버퍼만 계산해도 대략 가로 × 세로 × 4바이트 규모다.
실제 사용량은 디코더와 브라우저 구현에 따라 더 달라진다.
디스크에서는 작은 파일이어도 해상도가 아주 크면 브라우저 탭이 버거울 수 있는 이유다.
브라우저 안에서 읽고, 줄이고, 다시 묶었다
처리 흐름은 세 단계였다.
압축 파일을 픽셀로 푸는 디코딩, 작은 캔버스에 다시 그리는 축소, 결과를 WebP 파일로 묶는 인코딩이다.
코드를 읽지 않아도 핵심은 같다.
원본을 ImageBitmap으로 읽고, 비율을 유지해 작은 캔버스에 그린 다음, 그 결과만 WebP로 만든다. 아래 코드는 내부 이름과 저장 경로를 뺀 단순화 예시다.
type CompressionOptions = {
maxSize: number;
quality: number;
};
export async function compressImage(
file: File,
{ maxSize, quality }: CompressionOptions,
) {
const bitmap = await createImageBitmap(file);
try {
const ratio = Math.min(
1,
maxSize / bitmap.width,
maxSize / bitmap.height,
);
const width = Math.max(1, Math.floor(bitmap.width * ratio));
const height = Math.max(1, Math.floor(bitmap.height * ratio));
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
if (!context) throw new Error('이미지 처리 환경을 만들 수 없습니다.');
context.drawImage(bitmap, 0, 0, width, height);
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(result) => result
? resolve(result)
: reject(new Error('이미지 인코딩에 실패했습니다.')),
'image/webp',
quality,
);
});
if (blob.type !== 'image/webp') {
throw new Error('이 브라우저에서는 WebP 인코딩을 지원하지 않습니다.');
}
return new File([blob], 'optimized.webp', { type: blob.type });
} finally {
bitmap.close();
}
}createImageBitmap()은 파일을 화면에 그릴 수 있는 픽셀 데이터로 읽는다.
canvas는 축소한 그림을 잠시 그려 두는 작업 공간이고, toBlob()은 그 결과를 파일처럼 다룰 수 있는 데이터 묶음으로 내보낸다.
실패하면 toBlob()이 null을 줄 수 있어 오류로 처리했고, 마지막에는 ImageBitmap.close()로 그래픽 메모리를 돌려줬다.
quality: 0.9는 사람이 느끼는 화질이 정확히 90%라는 뜻이 아니다.
브라우저 인코더에 전달하는 압축 품질 힌트이므로 같은 값이어도 사진 내용과 브라우저에 따라 결과 크기와 화질이 달라질 수 있다.
MIME 형식은 image/webp처럼 파일이 어떤 종류라고 표시하는 값이다.
WebP를 요청했더라도 반환된 Blob의 MIME 형식을 확인해야 브라우저가 만든 결과와 확장자가 어긋나지 않는다.
사진 방향도 살펴봐야 했다.
EXIF는 카메라가 파일에 함께 넣는 촬영 시각·위치·방향 같은 정보다.
createImageBitmap()은 기본적으로 EXIF 방향을 반영해 픽셀을 읽지만, 캔버스에서 새 WebP를 만들면 원본의 촬영 정보까지 그대로 보존되는 것은 아니다.
원본 메타데이터나 정확한 색이 필요한 서비스라면 별도 보존 경로가 필요하다.
maxSize와 quality는 화면 요구사항에서 정한다
긴 변 1000px과 품질 0.9를 다른 서비스에 그대로 복사하면 안 된다.
먼저 이미지가 실제로 표시될 최대 CSS 크기와 고밀도 화면을 고려한다.
예를 들어 본문에서 최대 800px 너비로 보이고 2배 픽셀 밀도까지 선명하게 지원하려면 1600px 안팎의 파생본을 시험할 수 있다.
이 값도 디자인과 확대 기능에 따라 달라진다.
quality는 0과 1 사이에서 WebP 인코더에 보내는 힌트다.
0.9를 넣었다고 원본 화질의 90%가 보존되는 것은 아니다.
같은 값도 글자가 많은 캡처, 노이즈가 많은 야간 사진, 단색 일러스트에서 결과가 다르다.
대표 샘플을 0.75, 0.82, 0.9처럼 몇 구간으로 만들어 사람이 확대해 보고, 허용 가능한 품질 중 가장 작은 파일을 고르는 편이 낫다.
화면 캡처처럼 가는 글자가 많은 이미지는 사진과 같은 설정에서 번져 보일 수 있다.
투명 배경이 필요하다면 JPEG는 후보에서 빠진다.
서버와 대상 브라우저가 AVIF를 안정적으로 처리한다면 더 작은 결과를 시험할 수 있지만, 변환 시간과 호환 경로까지 함께 측정해야 한다.
이번 작업의 선택 기준은 “가장 작은 파일”이 아니라 다음 네 조건을 동시에 통과하는 값이었다.
- 서비스 화면에서 필요한 픽셀 수를 남긴다.
- 글자와 윤곽이 사용 목적에 맞게 보인다.
- 브라우저 변환이 업로드 절약 시간보다 오래 걸리지 않는다.
- 서버가 최종 바이트와 픽셀 크기를 다시 검증할 수 있다.
160.04MB가 2.49MB가 됐다
| 구분 | 값 |
|---|---|
| 샘플 수 | 4장 |
| 원본 합계 | 160.04MB |
| 변환 결과 합계 | 2.49MB |
| 줄어든 용량 | 157.55MB |
| 원본 대비 감소율 | 약 98.44% |
| 결과 파일 중 최대 크기 | 0.97MB |
| 적용한 변환값 | 긴 변 1000px, WebP 품질 0.9 |
네 장을 같은 설정으로 변환하자 업로드 대상은 원본의 약 64.3분의 1이 됐다.
긴 변 1000px와 품질 0.9는 당시 작업에 적용한 값이지, 모든 사진에 그대로 쓸 표준값은 아니다.
maxSize는 남길 픽셀 수를, quality는 WebP 인코더의 압축 정도를 바꾸므로 실제 화면 크기와 허용 화질에 맞춰 정해야 한다.
전송 구간만 단순 계산하면 차이는 더 분명하다.
1MB를 100만 바이트, 1Mbps를 초당 100만 비트로 놓을 때 10Mbps 업로드 환경에서는 약 128초 분량이 약 2초 분량으로 줄어든다.
이 값은 실제 완료 시간의 실측치가 아니다.
실제 업로드에는 브라우저의 WebP 변환 시간, 통신 지연과 서버 처리 시간이 더해진다.
어떤 경우에 브라우저 압축을 기본 경로로 둘 수 있는가
이 방식은 휴대전화 원본처럼 화면 표시 크기보다 큰 사진을 받고, 원본 픽셀과 촬영 정보를 반드시 보존할 필요가 없을 때 잘 맞는다.
프로필 이미지, 게시글 첨부 사진이나 화면 표시용 미리보기처럼 최종 크기가 정해진 경우다.
반대로 원본 사진을 보관해야 하거나 인쇄·보정처럼 색 정확도가 중요한 서비스에서는 축소본만 만드는 용도로 제한한다.
큰 사진을 픽셀로 푸는 순간 메모리를 사용하므로 저사양 기기에서 여러 장을 동시에 처리하는 경로도 피해야 한다.
98.44%는 모든 사용자 사진의 평균이 아니라 이번 샘플 네 장의 결과다.
운영 기본값으로 두기 전에는 서로 다른 기기와 사진으로 다음 조건을 확인한다.
- 변환 뒤에도 방향과 색이 허용 범위 안에서 유지되는가
- 저사양 기기에서 여러 장을 처리해도 탭이 종료되지 않는가
- WebP를 만들지 못했을 때 원본 전송 또는 업로드 중단 중 의도한 경로로 가는가
- 서버가 결과 파일의 실제 형식·크기와 픽셀 수를 다시 검사하는가
브라우저 축소는 전송량을 줄이는 사용자 편의 기능이고, 서버 검증을 대신하는 보안 경계는 아니다.
클라이언트가 보낸 MIME 형식과 파일명만 믿지 않고 파일 시작 바이트로 형식을 식별하고, 실제로 이미지를 디코딩할 수 있는지와 픽셀 크기 제한까지 다시 확인해야 한다.
결론: 전송할 픽셀을 먼저 줄였다
이번 샘플에서는 원본을 서버에 보낸 뒤 압축하는 대신, 브라우저에서 긴 변을 1000px로 줄이고 WebP로 다시 만든 뒤 업로드했다.
네 장의 전송 대상은 160.04MB에서 2.49MB로, 약 98.44% 줄었다.
원본 보존이 필요 없는 화면 표시용 사진에는 브라우저 축소를 기본 경로로 둘 수 있다.
원본이나 정확한 색 정보가 중요한 서비스에서는 미리보기 생성에만 사용한다.
이번에 효과가 컸던 이유는 품질 값을 무작정 낮춘 데 있지 않았다.
화면에서 쓰지 않을 픽셀을 먼저 덜어 낸 뒤, 실제로 필요한 크기의 파일만 서버로 보냈기 때문이다.