이 글은 img-compressor의 browser-image-processing.ts와 품질 옵션, 벤치마크 기록을 기준으로 브라우저에서 이미지를 리사이즈하고 WebP로 변환하는 전체 과정을 정리합니다. Canvas API의 일반적인 장점을 나열하기보다, 이 구현이 파일을 어떤 순서로 바꾸고 왜 품질 옵션을 좁게 두었는지에 집중합니다.

파일을 Canvas에 그리기까지의 네 단계

브라우저는 File을 바로 Canvas에 그릴 수 없습니다. 현재 함수는 FileReader.readAsDataURL로 파일을 읽고, 결과 Data URL을 Imagesrc로 넣은 뒤, 이미지가 로드되면 Canvas에 그립니다. 마지막으로 canvas.toBlob 결과를 다시 File로 감쌉니다.

const reader = new FileReader();
reader.onload = (event) => {
  const image = new Image();
  image.onload = () => {
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    context?.drawImage(image, 0, 0, canvas.width, canvas.height);
    canvas.toBlob((blob) => {
      if (!blob) return;
      resolve(new File([blob], 'result.webp', { type: 'image/webp' }));
    }, 'image/webp', quality);
  };
  image.src = event.target?.result as string;
};
reader.readAsDataURL(file);

각 비동기 경계에는 실패할 수 있는 지점이 있습니다. 파일 읽기 실패, 이미지 디코딩 실패, Canvas context 미획득, toBlob 결과 없음은 각각 다른 상태이므로 Promise를 reject해 호출부가 실패를 감지하도록 합니다. 실패 흐름 전체는 에러 핸들링 가이드에서 더 자세히 다룹니다.

비율 계산은 확대를 막는 쪽으로 고정한다

가로와 세로가 모두 목표 크기 안에 들어오도록, 두 비율 중 더 작은 값을 고르고 1보다 커지지 않게 제한합니다. 이 조건이 없으면 작은 이미지를 큰 Canvas에 그리는 순간 용량과 처리량이 불필요하게 커질 수 있습니다.

const scale = Math.min(
  1,
  maxWidth / image.width,
  maxHeight / image.height
);

canvas.width = Math.max(1, Math.round(image.width * scale));
canvas.height = Math.max(1, Math.round(image.height * scale));

최소값을 1로 두는 것도 작은 입력이나 극단적인 옵션에서 Canvas 크기가 0이 되는 것을 막기 위한 방어입니다.

품질 옵션은 왜 넓게 열어 두지 않았나

이미지 압축 UI에서 품질을 1부터 100까지 자유롭게 고르게 만들 수 있습니다. 하지만 실제 사용자는 숫자 하나하나의 차이를 판단하기 어렵습니다. 품질 73과 76의 차이를 화면에서 바로 구분하기도 어렵고, 파일 종류에 따라 같은 숫자가 전혀 다른 결과를 만들 수 있습니다. 그래서 img-compressor의 콘텐츠와 벤치마크는 품질값을 좁은 비교 범위로 다룹니다. 너무 많은 옵션은 정교함처럼 보일 수 있지만, 실제 도구에서는 결정을 늦추는 소음이 되기도 합니다.

사진과 스크린샷은 같은 품질값에서도 다른 결과를 낸다

고정 사진 자산은 원본 2,901,456바이트에서 WebP 품질 60 기준 148,680바이트까지 줄었습니다. 숫자만 보면 매우 큰 감소지만, 이 결과를 모든 이미지에 일반화하면 안 됩니다. 스크린샷, 투명 그래픽, 이미 최적화된 파일은 전혀 다른 결과를 보입니다.

자산원본결과 예시판단
사진 PNG2,901,456 BWebP q60 148,680 B포맷 전환 효과가 큼
스크린샷 PNG54,371 BWebP q80 31,954 B감소는 있지만 절대 차이는 작음
투명 그래픽 PNG9,838 BWebP q80 7,430 B품질보다 호환성 판단이 중요

이 표는 품질 옵션을 고를 때 파일 종류를 먼저 봐야 한다는 점을 보여 줍니다. 인코더와 측정 조건을 포함한 전체 수치는 벤치마크 글에 따로 정리되어 있습니다.

최대 크기 옵션은 리사이즈 판단과 연결된다

압축률은 인코더 품질만으로 정해지지 않습니다. 원본 이미지가 너무 크면 먼저 가로·세로를 줄이는 것이 더 큰 효과를 만들 수 있습니다. img-compressor는 최대 너비와 높이를 기준으로 비율을 유지해 리사이즈한 뒤, 필요할 때만 추가 압축을 수행합니다.

const resizedFile = await resizeImageFile(file, maxWidth, maxHeight);

if (resizedFile.size < 100 * 1024) {
  return resizedFile;
}

작아진 파일이 이미 100KB 미만이면 추가 손실 압축을 건너뛰는 조건도 같은 맥락입니다. 작은 파일에 추가 압축을 반복하면 체감 이득보다 품질 변화와 처리 시간이 더 눈에 띌 수 있습니다.

변환 결과가 항상 더 작지는 않다

WebP 변환 함수는 새 파일을 만들 수 있지만, 호출부는 그 파일이 언제나 더 낫다고 가정하지 않습니다. 직접 WebP 모드에서도 변환된 파일 크기가 리사이즈 파일보다 크거나 같으면 기존 파일을 유지합니다. 이 조건이 없으면 사용자는 “압축” 버튼을 눌렀는데 더 큰 파일을 받게 됩니다.

물론 용량만이 모든 품질 기준은 아닙니다. 어떤 경우에는 WebP 파일이 조금 더 커도 브라우저 호환성, 이미지 품질, 후속 처리 요구 때문에 선택될 수 있습니다. 그러나 현재 도구의 기본 목적이 “브라우저에서 가볍게 파일 크기를 줄이는 것”이라면, 더 큰 결과를 자동으로 제공하지 않는 편이 자연스럽습니다.

리사이즈와 포맷 변환을 한 함수에 섞지 않은 이유

저장소에서는 resizeImageFileconvertToWebPFile을 분리합니다. 전자는 원본 형식을 유지하며 필요한 경우에만 가로·세로를 줄이고, 후자는 Canvas 결과를 image/webp 파일로 만듭니다. 호출부가 두 결과를 비교할 수 있으므로 “크기만 줄일지”, “포맷도 바꿀지”를 UI 옵션과 연결하기 쉬워집니다.

분리했다고 해서 모든 조건이 자동으로 해결되는 것은 아닙니다. 원본의 메타데이터를 보존해야 하는지, 투명 영역을 어떤 형식으로 전달해야 하는지, 애니메이션 파일을 허용할지는 제품 요구 사항입니다.

처리 시간도 사용자 경험이다

용량을 조금 더 줄이기 위해 너무 오래 처리하면 도구 사용성이 떨어집니다. 특히 여러 파일을 순서대로 처리할 때는 한 장의 처리 시간이 전체 작업 시간을 결정합니다. 현재 사이트의 벤치마크는 주로 출력 바이트를 기록하며, 같은 자산으로 처리 시간까지 남기는 것은 다음 확인 항목으로 남겨 두었습니다. 그래야 품질 60과 80 사이의 차이를 용량뿐 아니라 기다리는 시간까지 포함해서 판단할 수 있습니다.

자주 받는 질문

품질 슬라이더 범위를 더 넓히면 안 되나요? 넓힐 수는 있지만, 그 전에 최소·최대 값이 실제 사용 목적에 맞는지부터 다시 봐야 합니다. 압축률이 크게 나오는 사진 예시만 보여 주면 작은 파일에서는 같은 설정이 거의 효과가 없다는 사실이 가려집니다.

리사이즈와 품질 옵션을 같이 켜면 결과가 꼬이지 않나요? 두 조건이 충돌하지 않는지는 직접 테스트로 확인해야 합니다. 결과가 원본보다 커졌을 때 그 결과를 자동으로 채택하지 않는 방어 조건이 있는지, 사용자가 받는 파일이 어떤 형식인지 버튼과 파일명에서 구분되는지도 같은 맥락의 질문입니다.

실제로 깨뜨려 보면서 확인한 것

손상된 파일이나 브라우저가 디코딩하지 못하는 파일을 일부러 넣어 실패가 호출부까지 제대로 전달되는지 봤고, 가로·세로가 큰 사진과 이미 작은 아이콘을 같은 배치에 넣어 리사이즈·품질 조건이 함께 맞물려 동작하는지도 확인했습니다. 자세한 실패 처리 기준은 에러 핸들링 가이드에, 결과 카드와 다운로드 흐름은 다중 파일 압축 UI 글에 이어집니다.

전체 구현은 convertToWebPFile과 resizeImageFile, 품질별 벤치마크 데이터에서 그대로 읽을 수 있습니다.