이 글은 img-compressor 공개 저장소의 구현과 고정 테스트 자산을 바탕으로 정리한 개발 기록입니다. 사용자 파일이나 고객 데이터는 사용하지 않았고, 수치는 저장소에 포함된 테스트 이미지에서 측정한 값만 적었습니다.
문제: 업로드하지 않고도 쓸 수 있는 이미지 처리 흐름
이미지 압축 도구를 만들 때 먼저 정한 경계는 파일을 서버로 보내지 않는 것이었습니다. 선택한 파일은 브라우저의 FileReader, Image, canvas, URL.createObjectURL만으로 처리합니다. 이 선택은 서버 저장소와 업로드 API를 줄여 주지만, 반대로 브라우저 메모리, 처리 중 상태, 다운로드 파일 관리까지 화면에서 책임져야 한다는 뜻이기도 합니다.
현재 구현은 여러 파일을 받아 각 파일을 순서대로 처리합니다. 처리 중에는 전체 개수와 현재 번호를 표시하고, 결과가 만들어진 뒤에만 개별 다운로드와 일괄 다운로드를 노출합니다. 파일 선택부터 결과 확인까지의 상태가 한 화면에서 이어지도록 한 이유는 사용자가 “압축이 끝났는지”, “어떤 파일이 줄었는지”, “원본보다 커진 결과는 없는지”를 바로 확인할 수 있게 하기 위해서입니다.
처리 순서: 축소할 수 있는 크기부터 먼저 줄인다
- 입력 파일을 읽고 이미지의 실제 가로·세로 크기를 확인합니다.
- 최대 너비와 높이를 넘는 경우에만 비율을 유지해 리사이즈합니다.
- 리사이즈 결과가 100KB 미만이면 추가 손실 압축을 건너뜁니다.
- 그 외 파일은
browser-image-compression을 동적으로 불러와 Web Worker 옵션으로 처리합니다. - 결과 파일이 리사이즈 결과보다 크면 압축 결과를 버리고 더 작은 쪽을 유지합니다.
const resizedFile = await resizeImageFile(file, maxWidth, maxHeight);
if (resizedFile.size < 100 * 1024) {
return resizedFile;
}
const { default: imageCompression } = await import('browser-image-compression');
const compressedFile = await imageCompression(resizedFile, {
maxSizeMB: 1,
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
useWebWorker: true,
fileType: resizedFile.type,
initialQuality: quality,
});
return compressedFile.size >= resizedFile.size ? resizedFile : compressedFile;
여기서 중요한 기준은 “항상 압축한다”가 아닙니다. 이미 작은 파일에 다시 인코더를 적용하면 용량 이득이 거의 없거나 화질만 달라질 수 있습니다. 또 라이브러리 결과가 더 크면 사용자에게 불리하므로, 결과 크기를 다시 비교하는 방어 조건을 넣었습니다.
WebP는 별도의 선택지로 둔다
현재 도구는 일반 압축 흐름과 WebP 변환 흐름을 분리합니다. 직접 WebP 모드를 선택하면 리사이즈 뒤 Canvas로 WebP 파일을 만들고, 그 결과가 더 큰 경우에는 변환 결과를 채택하지 않습니다. 일반 압축을 먼저 한 뒤 필요한 파일만 WebP로 바꾸는 흐름도 제공합니다. 포맷 전환이 언제나 이득이라는 가정을 코드에 넣지 않은 것입니다.
이 처리는 포맷·이미지 내용·품질값에 따라 결과가 달라진다는 전제를 둡니다. 사진, 스크린샷, 투명 그래픽은 압축 특성이 다르므로 같은 설정을 모든 파일에 권장하지 않습니다. 다음 글에서 Canvas 변환 함수의 동작을, 벤치마크 글에서 고정 자산별 수치를 따로 정리합니다.
브라우저 메모리를 정리하는 위치
결과 미리보기에는 URL.createObjectURL을 사용합니다. 이 URL은 편하지만 파일 목록을 비우거나 개별 항목을 삭제할 때 해제하지 않으면 브라우저 메모리에 남을 수 있습니다. 그래서 전체 초기화와 개별 삭제 로직에서 각각 URL.revokeObjectURL을 호출합니다. 단일 이미지 예제에서는 보이지 않기 쉬운 문제지만, 여러 장을 반복 처리하는 도구에서는 사용자 경험과 직결됩니다.
const clearAll = () => {
images.forEach((image) => {
URL.revokeObjectURL(image.url);
if (image.webpUrl) URL.revokeObjectURL(image.webpUrl);
});
setImages([]);
};
결과 카드에 남기는 정보
처리가 끝난 파일은 원본 파일명, 원본 크기, 출력 크기, 감소율, 선택된 형식을 함께 가집니다. 감소율은 1 - compressedSize / originalSize로 계산하고 음수 결과는 0으로 제한합니다. 이 값은 이미지 품질을 평가하는 점수는 아니지만, 사용자가 파일별로 어떤 결과를 받을지 판단하는 기본 정보가 됩니다.
직접 WebP 모드에서는 출력 카드에 WebP 파일을 바로 넣고, 일반 압축 모드에서는 원래 형식의 압축 결과를 먼저 보여 줍니다. 이후 개별 변환이나 일괄 변환을 선택했을 때만 WebP 다운로드 버튼이 나타납니다. 같은 화면에서 두 결과를 섞지 않도록 한 이유는 사용자가 현재 다운로드하는 파일이 무엇인지 알 수 있게 하기 위해서입니다.
이 흐름을 다시 확인하는 방법
- 가로 또는 세로가 최대 크기보다 큰 이미지와 작은 이미지를 각각 선택합니다.
- 작은 파일이 추가 압축 없이 결과 목록에 들어가는지 확인합니다.
- 품질값을 바꾼 뒤 WebP 변환 결과가 원본보다 커질 때 기존 파일이 유지되는지 확인합니다.
- 여러 파일을 처리하며 진행률이 현재 파일 번호와 전체 개수를 보여 주는지 확인합니다.
- 파일 하나를 지우고 전체를 비운 뒤, 새 파일을 다시 선택해 미리보기와 다운로드가 정상인지 확인합니다.
확인한 범위와 남은 한계
- 테스트 결과는 저장소의 고정 자산과 기록된 바이트 수를 기준으로 합니다. 모든 사진에서 같은 감소율을 보장하지 않습니다.
- 현재 구현은 일반 이미지 파일을 대상으로 하며, 애니메이션·색상 프로필·메타데이터 보존 요구는 별도 테스트가 필요합니다.
- 대형 파일을 여러 장 처리할 때의 체감 시간은 기기 성능과 브라우저에 따라 달라집니다.
- 처리 중 상태와 실패 처리는 화면에 남기되, 콘솔 오류만으로 끝나지 않도록 사용자용 오류 안내를 추가로 점검할 여지가 있습니다.