대부분의 웹 앱은 여전히 이미지 업로드를 블랙박스처럼 처리합니다. 사용자가 파일을 드롭하면 브라우저는 이를 전송하고, 서버는 페이로드를 수락하거나 아무도 대비하지 못한 413 에러를 던집니다. 브라우저 측 압축은 이 공식을 바꿉니다. 네트워크로 전송되기 전에 페이로드를 줄일 기회를 제공하며, 이는 더 빠른 업로드, 낮은 대역폭 비용, 그리고 서버 타임아웃 감소를 의미합니다. 하지만 이 작업은 실수하기 쉽습니다. 압축을 단순히 '품질(quality)'이라고 적힌 마법의 슬라이더처럼 다룬다면, 깨진 이미지, 늘어진 썸네일, 그리고 혼란스러운 사용자 경험을 제공하게 될 것입니다. 더 나은 접근 방식은 전체 흐름을 하나의 파이프라인으로 취급하는 것입니다.
Think in Pipelines, Not Sliders
작업을 개별 단계로 나눕니다. input 요소에서 파일을 읽고, 이미지를 목표 크기로 축소하고, 새로운 Blob을 인코딩한 다음, 결과를 사용자에게 다시 렌더링합니다. 각 단계는 한 가지 일만 수행하고 그 출력을 다음 단계로 전달합니다. 이러한 분리는 단순히 코드를 깔끔하게 만드는 데 그치지 않습니다. 단위 테스트를 수월하게 만들어 줍니다. 파일 input을 건드리지 않고도 알려진 버퍼를 스케일링 단계에 입력할 수 있습니다. 서버의 응답을 기다릴 필요 없이 인코더가 200 KB 미만의 JPEG를 생성하는지 확인할 수 있습니다. 문제가 발생했을 때 어떤 단계에서 실패했는지 정확히 알 수 있습니다.
이러한 역할을 분리하면 업로드 중 예기치 못한 상황을 방지할 수 있습니다. 스케일링과 인코딩을 하나의 엉킨 함수로 묶어버리면, 중간에 디코딩 에러가 발생했을 때 업로드 큐가 일관성 없는 상태로 남을 수 있습니다. 파이프라인은 모든 경계에서 검증을 강제합니다. 파일을 디코딩할 수 없다면 캔버스를 생성하기 전에 잡아낼 수 있고, 인코딩된 Blob이 너무 크다면 서버에 저장을 요청하기 전에 잡아낼 수 있습니다.
Define a Contract Before You Write Code
누군가 canvas draw 호출을 작성하기 전에, 규칙을 작성하고 팀과 공유하십시오. 허용할 MIME 타입을 선택하십시오. JPEG, PNG, WebP, AVIF 중 무엇을 허용할 것입니까? 각 타입은 알파 채널, 브라우저 지원 및 CPU 비용에 영향을 미칩니다. 최대 입력 크기를 설정하십시오. 플래그십 스마트폰에서 찍은 30 MB짜리 원본 사진을 메모리에서 통째로 디코딩하려고 하면 오래된 노트북은 멈추거나 충돌할 수 있습니다. 최대 출력 크기를 정의하십시오. UI에서 2048 픽셀보다 넓은 이미지를 표시할 일이 없다면, 6000 픽셀 너비의 사진을 파이프라인에 통과시킬 이유가 없습니다.
가장 중요한 것은 디코딩 실패에 대비하는 것입니다. 손상된 파일, 생소한 컬러 프로필 또는 잘린 업로드는 Image 생성자에서 에러를 발생시킬 수 있습니다. 파이프라인에는 명확한 catch 블록과 사람이 읽을 수 있는 에러 메시지가 필요합니다. 브라우저가 조용히 멈춰버려 사용자가 아무 일도 일어나지 않는 스피너만 바라보게 만들지 마십시오.
Respect the Image
왜곡된 이미지는 아마추어처럼 보입니다. 종횡비(aspect ratio)를 유지하고 긴 쪽의 길이를 제한하십시오. 목표 영역이 1024x1024 픽셀이라면, 4000x3000 사진은 1024x1024가 아니라 1024x768이 되어야 합니다. 긴 쪽의 가장자리에서 스케일 인자(scale factor)를 계산하고 짧은 쪽이 이를 따르도록 하십시오. 이렇게 하면 이미지가 이상한 모양으로 늘어나는 것을 방지할 수 있습니다.
실제 내보내기에는 canvas의 toBlob 메서드를 사용하십시오. 출력 형식과 품질 설정을 직접 제어할 수 있으며, 비동기로 실행되므로 메인 스레드를 차단하지 않습니다. 오프스크린 캔버스(offscreen canvas)를 생성하여 크기가 조정된 이미지를 그린 다음, 원하는 타입과 품질 값으로 canvas.toBlob을 호출하십시오. 생성된 새로운 Blob을 업로드 로직이나 스토리지 API에 전달하면 됩니다.
Show the Evidence
압축은 눈에 보이지 않는 작업입니다. 수치를 보여주지 않으면 사용자는 이 과정을 신뢰하지 않을 것입니다. 원본과 결과물을 비교할 수 있는 인터페이스를 구축하십시오. 원본 파일 크기, 새로운 파일 크기, 새로운 크기, 그리고 최종 포맷 타입을 표시하십시오. 4.2 MB의 휴대폰 사진이 380 KB의 WebP로 줄어드는 것을 보면, 사용자는 이미지가 몰래 훼손되고 있다는 불안감을 떨칠 수 있습니다.
이러한 투명성은 문제 해결에도 도움이 됩니다. 업로드 실패에 대해 사용자가 불만을 제기할 때, 가장 먼저 확인해야 할 것은 출력 크기가 서버 제한을 초과했는지, 아니면 포맷이 PNG에서 JPEG로 바뀌면서 알파 채널이 사라졌는지 여부입니다. 해당 데이터를 UI에 표시하여 사용자가 고객 지원 티켓을 열기 전에 스스로 문제를 진단할 수 있도록 하십시오.
Presets Beat Re-compression
같은 이미지를 두 번 압축하지 마십시오. 손실 압축(lossy) 인코더를 거칠 때마다 디테일이 더 많이 사라지고 아티팩트(artifacts)가 발생합니다. 사용자가 '최적화' 버튼을 반복해서 누르게 되면, 세 번째 결과물은 복사본의 복사본처럼 보일 것입니다. 대신, 모든 출력물을 원본 소스 파일로부터 생성하고 프리셋을 제공하십시오.
- 작은 파일: 썸네일이나 빠른 미리보기를 위해 품질을 낮추고 크기를 공격적으로 제한합니다.
- 균형 잡힌 설정: 소셜 피드 및 갤러리에 적합하도록 합리적인 크기와 적절한 품질 수준을 목표로 합니다.
- 더 많은 디테일: 사진, 예술 작품 또는 인쇄 미리보기를 위해 높은 품질과 큰 크기를 유지합니다.
사용자가 손실이 누적되지 않은 상태에서 프리셋을 전환할 수 있도록 원본 Blob을 메모리에 저장하세요. 항상 소스에서 생성해야 하며, 마지막 출력물로부터 생성해서는 안 됩니다.
사용자가 업로드하는 방식 그대로 테스트하세요
광섬유 연결과 32GB RAM을 갖춘 개발용 컴퓨터는 현실이 아닙니다. 실제 사용자가 사용하는 파일로 테스트하세요. iOS와 Android의 휴대폰 사진은 서로 다른 메타데이터 방향을 사용하며 HEIC 소스에서 생성되었을 수도 있습니다. 로고나 아이콘 같은 투명한 에셋은 JPEG 변환 시 다르게 동작하는데, 이는 JPEG가 알파 채널을 지원하지 않기 때문입니다. 거대한 파일은 2GB RAM을 가진 기기에서 메모리 제한 문제를 드러낼 것입니다. 느린 모바일 CPU는 toBlob 호출이 실제로 얼마나 걸리는지 정확히 보여줄 것입니다.
Chrome DevTools를 사용하여 CPU와 네트워크 속도를 제한(throttle)해 보세요. 5년 된 Android 폰으로 테스트해 보는 것도 좋습니다. 인코딩 중에 파이프라인이 UI를 3초 동안 멈추게 한다면, 인터페이스의 응답성을 유지하기 위해 무거운 작업을 Web Worker로 옮겨야 합니다.
기본 기능부터 먼저 출시하세요
모든 형식을 지원하고 싶은 유혹이 들겠지만
