이 글은 ImageCompressor 컴포넌트를 기준으로 작성한 구현 기록입니다. 단일 파일 예제가 아니라, 사용자가 여러 이미지를 한 번에 선택했을 때 진행 상태, 결과 카드, 다운로드 파일명이 어떻게 함께 움직여야 하는지에 집중합니다.
로딩 값 하나로는 부족했던 이유
이미지 압축 도구에서 가장 단순한 구현은 isLoading 하나를 두고, 처리 중이면 모든 버튼을 막는 방식입니다. 단일 파일 도구라면 이 정도로도 충분할 수 있습니다. 하지만 여러 이미지를 순서대로 압축하고, 완료된 항목은 바로 다운로드할 수 있게 만들려면 상태를 더 세밀하게 나눠야 합니다. 이 구분이 없으면 화면은 멈춘 것처럼 보이고, 버튼을 눌러도 되는지 판단하기 어렵습니다.
진행 상태는 현재 번호와 전체 개수를 함께 가진다
컴포넌트는 여러 파일을 반복하면서 현재 처리 번호와 전체 개수를 상태로 저장합니다. 단순한 퍼센트보다 이 방식이 나은 이유는 사용자가 몇 번째 파일에서 시간이 걸리는지 바로 알 수 있기 때문입니다. 큰 사진과 작은 아이콘이 섞여 있으면 파일별 처리 시간이 다르게 느껴집니다.
setProcessingProgress({
current: i + 1,
total: imageFiles.length,
type: 'compressing',
});
type 값도 함께 둔 이유는 같은 진행 영역에서 압축과 WebP 일괄 변환을 구분하기 위해서입니다. 라이브러리를 필요한 시점에만 불러오는 동적 import와 Web Worker 옵션은 성능 최적화 글에서 따로 다룹니다.
결과 목록은 완료된 파일의 기록이다
파일 처리 함수가 성공하면 결과 객체에는 원본 이름, 원본 크기, 출력 파일, 출력 크기, 미리보기 URL, 감소율 같은 값이 들어갑니다. 이 값들은 화면 표시뿐 아니라 다운로드 파일명을 만들 때도 사용됩니다. 실패한 파일을 같은 배열에 조용히 넣지 않는 것이 중요합니다. 처리하지 못한 파일이 결과 목록에 들어가면 사용자는 다운로드 버튼을 눌렀을 때 빈 파일이나 잘못된 파일을 받을 수 있습니다. 현재 구현은 실패 시 화면 메시지를 통해 흐름을 끊고, 성공한 파일만 결과 카드에 추가합니다.
개별 WebP 변환 상태는 카드 단위로 잠근다
압축이 끝난 뒤 사용자는 특정 파일만 WebP로 변환할 수 있습니다. 이때 전체 화면을 다시 로딩 상태로 바꾸면 다른 파일 다운로드까지 막히게 됩니다. 그래서 개별 변환 중인 항목은 파일 식별자에 가까운 값으로 따로 관리합니다.
const [convertingWebP, setConvertingWebP] = useState(null);
const handleConvertOne = async (image) => {
setConvertingWebP(image.id);
try {
const webpFile = await convertToWebPFile(image.file, quality);
// 해당 카드에만 WebP 결과를 붙인다.
} finally {
setConvertingWebP(null);
}
};
이 패턴은 사용자가 작업 범위를 이해하게 해 줍니다. 전체 작업인지, 특정 카드 작업인지, 이미 완료된 결과인지가 상태 이름과 UI 잠금 범위에 그대로 드러납니다.
일괄 다운로드는 완료 목록을 기준으로 한다
일괄 다운로드는 현재 결과 목록에 있는 파일만 대상으로 삼습니다. 처리 중인 파일을 예상해서 포함하지 않고, 완료된 결과만 다운로드합니다. 브라우저는 여러 다운로드를 짧은 시간에 연속 실행할 때 사용자 설정이나 보안 정책의 영향을 받을 수 있어, 구현에서는 각 다운로드 호출 사이에 약간의 간격을 둡니다. 이 처리는 성능 최적화라기보다 브라우저 동작을 더 안정적으로 만드는 사용자 경험상의 선택입니다.
다운로드 파일명은 원본 이름을 보존한다
브라우저에서 압축 파일을 만들면 결과는 메모리 안의 File 또는 Blob입니다. 파일명이 모두 download.webp처럼 나오면, 여러 장을 처리한 뒤 어떤 파일이 어떤 원본에서 왔는지 알기 어렵습니다. 좋은 기준은 원본 이름의 핵심을 유지하되, 출력 형식과 처리 결과를 구분할 수 있게 하는 것입니다.
const baseName = originalName.replace(/\.[^/.]+$/, '');
const webpName = baseName + '.webp';
const webpFile = new File([blob], webpName, { type: 'image/webp' });
압축 후에도 원본 형식을 유지하는 흐름과 WebP로 변환하는 흐름은 다릅니다. 원본 형식을 유지했다면 기존 확장자를 그대로 둘 수 있지만, WebP 변환 결과라면 확장자를 .webp로 바꿔야 합니다. 파일명만 원본을 닮고 실제 MIME type이 다르면 후속 작업에서 혼란이 생깁니다. 서로 다른 폴더에서 온 파일이 같은 이름을 가질 수도 있는데, 브라우저 입력은 경로를 노출하지 않기 때문에 여러 파일을 한 번에 받을 때 중복 이름이 있으면 운영체제나 브라우저가 자동으로 번호를 붙일 수 있습니다.
결과 카드의 숫자가 사용자를 속이지 않는지 본다
"줄었습니다"라고만 표시하면 사용자는 무엇이 얼마나 좋아졌는지 알 수 없습니다. 원본이 3MB였는지 30KB였는지에 따라 같은 20% 감소도 의미가 다릅니다. 그래서 결과 카드에는 원본 크기, 출력 크기, 감소율을 함께 보여 주는 것이 좋습니다.
const reduction = Math.max(
0,
Math.round((1 - outputSize / originalSize) * 100)
);
결과가 더 커졌을 때 음수 감소율을 그대로 보여 주면 사용자가 혼란스러울 수 있어, 표시값도 0보다 작아지지 않게 제한합니다. WebP로 변환한 결과와 일반 압축 결과를 같은 자리에 섞어 보여 주면 사용자는 지금 보는 값이 어떤 처리의 결과인지 헷갈릴 수 있고, 출력 파일이 더 커진 경우를 다른 결과와 똑같이 "성공"처럼 표시하면 카드 자체가 거짓 정보가 됩니다. 벤치마크 표와 결과 카드는 목적이 다릅니다. 벤치마크는 고정 자산과 조건을 설명하는 자료이고, 결과 카드는 사용자가 방금 선택한 파일의 처리 결과입니다.
이 설계가 맞았는지는 어떻게 알 수 있나
파일 선택 전과 압축 중, 압축 완료 상태가 화면에서 분명히 구분되는지, 여러 파일을 처리하는 동안 현재 번호와 전체 개수가 계속 보이는지부터 확인했습니다. 실패한 파일이 성공 목록에 조용히 섞여 들어가면 이 구조는 의미가 없으므로 그 지점도 따로 봤고, 특정 카드의 WebP 변환이 전체 다운로드 버튼까지 불필요하게 막지는 않는지도 확인했습니다. 확장자가 없는 파일명, 한글과 공백이 섞인 파일명, 점이 여러 개 들어간 파일명을 일부러 넣어 보며 결과 이름이 깨지지 않는지도 같은 점검에서 봤습니다. 전체 삭제 후에는 Object URL이 정리되고 새 파일 처리에 이전 결과가 남지 않아야 한다는 것도 함께 확인했는데, 정리 시점과 누락 사례는 Object URL 메모리 누수 가이드에서 자세히 다룹니다.
이 구현의 한계
현재 방식은 순차 처리를 기준으로 설명합니다. 대량 파일을 더 빠르게 처리하려면 병렬 처리나 작업 큐를 고려할 수 있지만, 그때는 메모리 사용량, 취소 처리, 실패한 파일만 재시도하는 흐름을 함께 설계해야 합니다. 또한 진행률은 파일 개수 기준이지 실제 처리 시간 기준이 아닙니다. 10개 중 5개를 처리했다고 해서 시간의 절반이 지났다는 뜻은 아니므로, 화면 문구도 정밀한 시간 예측처럼 보이지 않게 유지하는 것이 좋습니다. 여러 파일 처리와 결과 카드 상태, 그 아래의 브라우저 이미지 처리 함수는 자료실에서 그대로 읽을 수 있습니다.