이미지 처리를 브라우저 안에서 끝내기 — 무엇을 옮겼고 무엇을 못 옮겼나
이미지를 처리하려면 먼저 업로드해야 한다——이 습관은 서버 기반 도구가 들인 겁니다. 평소에 하는 일을 세어 보면, 용량 줄이기·포맷 변환·배경 따기·짧은 움짤 만들기·이미지 속 글자 뽑기 정도인데 그 대부분은 브라우저 혼자서도 됩니다. 저희는 그걸 하나씩 옮겼고, 중간에 모양새가 좋지 않은 경계에 부딪혔습니다. 먼저 제 담당을 밝혀두면, 클라이언트 사이드 코덱과 모델 로딩이 제 몫이고 배경 제거 알고리즘과 PDF 엔진은 제가 만든 게 아닙니다. 그 두 부분은 관찰한 현상만 이야기하겠습니다.
왜 서버에 두지 않았나
사용자의 이미지를 건드리고 싶지 않아서입니다. 온라인 이미지 도구는 거의 다 업로드·처리·다운로드 구조입니다. 파일이 상대 서버에 들어간 뒤 얼마나 보관되는지, 누가 볼 수 있는지, 정말 지워지는지는 전부 상대의 말에 달려 있습니다. 개인정보 방침은 약속이고, 약속은 검증이 안 됩니다. 그런데 사람들이 정작 처리하려는 건 밖으로 돌아다니면 안 되는 것들입니다——신분증, 계약서, 아직 안 올린 상품 사진, 얼굴이 들어간 소재. 브라우저 안에서 돌면 이 문제가 없고 파일이 기기를 한 번도 떠나지 않습니다. 게다가 이 주장은 검증됩니다. DevTools의 Network 패널을 열고 이미지 한 장을 처리해 보면 업로드 요청이 없고, 미리보기의 src는 blob:으로 시작합니다. 브라우저 메모리에 있는 객체라 네트워크를 탄 적이 없다는 뜻입니다. 10초면 되고, 방침 한 페이지 읽는 것보다 확실합니다.
헷갈리기 쉬운 걸 짚어두면, 일부 기능은 처음 쓸 때 이쪽으로 뭔가를 내려받습니다. AI 모델이나 특정 포맷의 디코더입니다. 방향이 서버에서 브라우저로라서 "내 이미지를 밖으로 보내는 것"과는 반대입니다. Method만 봐도 구분됩니다. GET은 가져오는 것, POST가 보내는 것이죠. 비용 쪽 이점은 따라온 것일 뿐입니다. 처리가 사용자 기기에서 돌기 때문에 한 번 더 처리해도 저희 비용은 그대로고, 그래서 무료·무제한·가입 불필요·워터마크 없음이 가능합니다. 서버 방식은 호출마다 돈이 나가니 제한을 걸고 가입을 요구하고 결제로 유도할 수밖에 없습니다. 인색해서가 아니라 계산이 안 맞아서입니다. 대가도 분명합니다. 사용자 기기를 쓰고, 몇몇 포맷은 로컬에서 못 합니다. 저희는 "모든 기능이 업로드 없음"이라고 말하지 않습니다.
지금 브라우저 안에서 끝나는 것들
이미지 쪽은 압축(내용을 보고 파라미터를 자동으로 정하는데 스크린샷과 사진이 다른 프리셋을 받습니다), 포맷 변환(십수 종을 읽고 흔한 건 로컬에서 바로 뽑습니다), AI 배경 제거(모델은 필요할 때 받고 추론은 로컬), 움짤 작업실(GIF·APNG·애니메이션 WebP 상호 변환, 프레임별 시간 조정·삭제·재정렬), 다국어 OCR(글자 위치와 표의 행렬 구조 유지), 온라인 이미지 편집, AI 고해상도화. 문서 쪽은 PDF 압축(텍스트 레이어와 링크 보존), PDF를 이미지로, 내장 원본 이미지 추출, 내용 추출, PDF를 HTML로, 그리고 DOCX·PPTX를 오프라인 HTML로. 전부 기기 안에서 끝납니다. 주소는 imging.ai 입니다. 자체 구현 쪽 숫자를 두 개 들면, PNG-8 감색은 참고 샘플 256색에서 45.8dB이고 같은 샘플의 pngquant는 43.4dB, 움짤 프레임 간 차분은 공개 샘플 기준 같은 이미지가 843KB에서 350KB가 됩니다.
로컬에서 돌려도 되는 범위를 어떻게 정했나
처음부터 이런 확신이 있었던 건 아닙니다. "이 브라우저가 이 작업을 할 수 있나"를 UA와 호환성 표로 판단했습니다. 버전 보고 포맷 보고 조건이 맞으면 기능을 열어주는 식이죠. 그러다 어떤 앱의 내장 브라우저에서 크게 당했습니다. 그 환경은 WebP를 멀쩡히 표시하는데, canvas.toBlob으로 WebP를 요청하면 아무 말 없이 PNG를 돌려줍니다. 에러도 없고 확장자도 .webp 그대로라, 사용자는 용량이 안 줄었다며 문의를 해왔습니다. 문제는 "WebP를 지원한다"는 그 한 문장에 있었습니다. 표에서는 칸 하나지만 런타임에서는 여러 가지 별개의 일입니다——디코딩이 되는지, canvas로 인코딩해서 내보낼 수 있는지, 나온 게 정말 WebP인지. 표는 맞았고 틀린 건 저희 결론이었습니다.
고친 방식은 미련합니다. 추측을 그만두고 모든 기능을 사용자 브라우저에서 실제로 한 번씩 돌려봅니다. 쪼개면 여섯 항목이고 서로 독립입니다. 입력 디코딩, 캔버스 처리, 출력 인코딩, 멀티스레드 WASM, WebGPU, 저장. 진짜로 디코딩하고 진짜로 인코딩해서 양쪽이 다 통과해야 "즉시"로 표시하고, WASM을 받아야 도는 건 "단말", 둘 다 안 되는 것만 서버로 보냅니다. UI에 있는 세 가지 색 칩이 이 탐지 결과인데, "이 포맷을 지원합니다"가 아니라 "이 기기가 지금 할 수 있느냐"를 말합니다. 예상 못 한 부수효과도 있었습니다. 어떤 기능을 로컬에 올릴지가 기획 판단이 아니라 탐지 결과로 정해지게 됐습니다. 재보고 나서야 알게 된 것도 있습니다. 지원 안 되는 포맷 몇 개가 돌려주는 blob 크기가 정확히 같은 95바이트였는데, 같은 1×1 폴백 PNG였습니다. 이 숫자는 이후 "조용히 다운그레이드됐는지" 가려내는 저희 나름의 방법이 됐습니다.
못 옮긴 것들
HEIC 쓰기, TIFF, JPEG 2000은 서버로 갑니다. HEVC는 특허가 있고 인코더 용량도 감당이 안 돼서 로컬 경로가 성립하지 않습니다. UI에 색으로 표시해 두었으니 고르기 전에 보입니다. HTML을 PDF로 바꾸는 것도 변환을 누른 뒤 스크립트를 걷어낸 스냅샷을 같은 오리진의 헤드리스 Chromium으로 보내기 때문에 순수 로컬이 아닙니다. 이건 페이지에 그대로 적어두었습니다. 그래서 "이미지를 업로드하지 않는다"는 말에는 범위가 있고, 로컬 경로를 타는 기능에 한해 성립합니다. 동영상 압축은 며칠 전에 열린 거라 아직 안 돌려봤습니다.
아직 못 푼 것
탐지 자체에 비용이 있습니다. 첫 진입 때 실제로 디코딩과 인코딩을 한 번 돌리는데, 샘플 이미지가 아주 작아도 저사양 기기에서는 그 수백 밀리초가 느껴집니다. 결과를 캐시해도 봤지만 사용자가 브라우저를 바꾸거나 OS를 올리면 캐시가 틀려져서, 지금은 짧은 만료를 걸고 버티는 중입니다. 이 부분은 계속 못마땅한데 더 나은 방법도 못 찾았습니다. 매번 재면 경험이 나빠지고, 오래 캐시하면 다시 "추측"으로 돌아가는데, 그 추측이 애초에 없애려던 문제니까요.
