이미지 처리를 브라우저 안에서 끝내기 — 업로드도 서버 비용도 없이
동료가 상품 사진을 다루는 걸 옆에서 봤습니다. 하려는 건 단순했어요. 사진 십여 장을 누끼 따고 배경을 바꾼 뒤, 플랫폼이 요구하는 용량까지 줄이는 것.
두 가지 방법을 다 시도해 봤더군요.
포토샵은 설치돼 있었지만, 사진 십여 장 때문에 실행하고 로딩을 기다리다 보면 이미 하기 싫어집니다. 디자이너도 아니라서 쓰는 기능은 5%도 안 되고요.
온라인 AI 도구는 업로드 → 대기열 → 다운로드까지 한 장에 수십 초. 사람이 몰리는 시간대면 더 길어지고, 가장자리가 지저분하게 잘리면 처음부터 다시 돌려야 합니다.
그가 한 말은 이랬습니다. "그냥 빨리 끝내고 싶어요. 이거 하나 때문에 프로그램 깔기도 싫고, 멍하니 기다리기도 싫고."
기술 용어는 하나도 없지만 문제는 분명합니다. 기존 선택지는 무겁거나 느리거나. 그리고 둘 다 알고리즘 문제가 아닙니다.
무거움과 느림은 어디서 오는가
느림부터. 온라인 AI 도구의 경로는 이렇습니다.
사진 선택 → 업로드(내 업로드 대역폭에 종속) → 대기(서버 부하에 종속)
→ 처리 → 다운로드 → 결과 확인실제로 일하는 건 「처리」 한 구간뿐이고 앞뒤는 전부 대기입니다. 업로드 대역폭은 보통 다운로드보다 훨씬 좁아서 몇 MB짜리 사진 하나 올리는 데만 몇 초가 걸리고, 대기열은 운에 맡기는 영역이죠.
무거움은 설치형 소프트웨어의 시작 비용입니다. 1장을 처리하든 100장을 처리하든 먼저 실행을 기다려야 합니다. 헤비 유저라면 분산되지만, 가끔 십여 장 만지는 사람에게는 그냥 순손실입니다.
그래서 방향을 이렇게 잡았습니다. 처리 자체를 브라우저 안에서 끝낸다. 업로드도 대기열도 설치도 없애면, 위의 두 비용이 동시에 사라지니까요.
첫 번째 벽: 브라우저 능력은 조회할 수 없다
처리를 로컬에 두기로 하면 가장 먼저 할 일은 「이 브라우저가 실제로 뭘 할 수 있는지」 파악하는 겁니다. 여기서 크게 한 번 넘어졌습니다.
canvas로 WebP를 뽑는 코드는 이렇게 씁니다.
canvas.toBlob(blob => {
download(blob, 'output.webp');
}, 'image/webp');콜백은 정상적으로 호출되고, blob도 null이 아니고, 크기도 그럴듯합니다. 그런데 그게 WebP라는 보장은 없습니다.
HTML 명세에 이렇게 적혀 있습니다. 요청한 타입을 사용자 에이전트가 지원하지 않으면 PNG로 만들어야 한다. 에러도 경고도 없이 조용히 바뀝니다. 저는 "파일이 안 열린다"는 제보를 받고 16진수로 열어봤다가 앞머리가 89 50 4E 47(PNG 매직 넘버)인 걸 보고서야 알았습니다.
UA로 판별하면 되지 않느냐 — 안 됩니다. iOS에서는 모든 브라우저가 WebKit이고, WebView 버전은 OS를 따라갑니다. 사용자가 「데스크톱 사이트 요청」을 켜면 UA는 macOS가 됩니다. UA가 답하는 건 "너는 누구냐"이고, 제가 묻고 싶은 건 "지금 이 포맷을 인코딩할 수 있냐"입니다. 그 사이엔 커널 버전, OS 버전, 호스트 앱, 빌드 옵션이 끼어 있습니다.
게다가 canvas 인코딩에는 능력 조회 API 자체가 없습니다. 오디오·비디오 쪽엔 MediaRecorder.isTypeSupported()가 있는데 이미지 쪽엔 아무것도 없어요.
결국 방법은 하나뿐입니다. 실제로 한 번 인코딩해 보고 돌아온 타입을 대조하는 것.
const blob = await new Promise(r => canvas.toBlob(r, mime));
const ok = !!blob && blob.type === mime; // 뒷부분이 핵심데스크톱 Chrome에서 돌려본 결과는 이렇습니다. WebP와 JPEG는 정상 출력. AVIF, HEIC, TIFF, GIF는 전부 PNG로 폴백됐고, 폴백된 네 개는 돌아온 blob 크기가 완전히 똑같았습니다. 같은 PNG 한 장이니 당연하죠. blob이 null인지만 확인했다면 여섯 포맷 전부 "성공"으로 집계됐을 겁니다.
읽는 쪽도 마찬가지라, 디코딩된다고 인코딩되는 건 아닙니다. Safari 16은 AVIF를 디코딩하지만 인코딩은 못 합니다. 그래서 기준을 이렇게 정했습니다. 입력이 디코딩되고 출력이 인코딩된다, 둘 다 성립할 때만 로컬로 돌린다.
빠른 구현이 옳은 구현은 아니었다
PNG를 256색으로 줄이는 작업에서 이걸 체감했습니다.
160만 픽셀 × 팔레트 256색 = 약 4.1억 번의 거리 계산. 정직하게 완전 탐색으로 짜면 실측 958ms. 브라우저에서는 멈춘 것과 같습니다.
교과서적 해법인 상위 비트 양자화 룩업 테이블을 쓰면 12ms까지 떨어집니다. 77배 빠르죠. 그런데 정확도를 검증해 보니 14.57%의 픽셀이 최적이 아닌 색을 골랐습니다. 테이블 자체가 근사값이니 예정된 결과였습니다.
그래서 이미지를 다시 세어봤습니다. 160만 픽셀인데 고유 색은 18만 개뿐. 계산의 89%가 중복 작업이었던 겁니다.
완전한 RGB 값으로 캐싱하는 방식으로 바꾼 결과가 153ms, 오차 0. 완전 탐색보다 6배 빠르면서 품질은 하나도 양보하지 않았습니다.
필요했던 건 더 똑똑한 자료구조가 아니라 입력의 성질을 재보는 일이었습니다. 순서를 뒤집으면 빠르지만 틀린 구현에 안착하게 됩니다.
로컬에서 되는 범위는 생각보다 넓었다
처음엔 포맷 변환과 압축만 하려고 했는데, 만들다 보니 경계가 예상보다 멀었습니다.
움직이는 이미지가 뜻밖이었습니다. GIF, APNG, 애니메이션 WebP, 애니메이션 AVIF 네 포맷의 인코더를 결국 직접 구현했고, 프레임 단위 편집(순서 변경, 개별 프레임 시간 조절, 삭제)까지 됩니다. 기존 애니메이션을 압축할 때는 프레임별 표시 시간과 반복 설정을 읽어 보존하기 때문에 압축 후에 리듬이 망가지지 않습니다.
AI 누끼는 가장 회의적이던 부분입니다. 브라우저에서 모델 돌리는 건 장난감이라고 생각했거든요. 실제로는 모델이 온디맨드로 내려오고(빠른 등급 약 42MB, 그 외 219MB와 447MB 두 등급), 추론은 WebGPU 우선에 미지원 환경에서는 WASM으로 폴백합니다. 한 번 받으면 브라우저가 캐시하므로 두 번째부터는 초 단위로 끝납니다.
여기서 서두의 "느리다"는 불만이 회수됩니다. 경로에 업로드도 대기열도 없고, 두 번째부터는 모델조차 받을 필요가 없으니까요.
안 되는 것들
경계를 숨기면 사용자는 두 번만 써봐도 알아챕니다. 그때 잃는 건 도구 전체의 신뢰라서, 그냥 적어두는 쪽을 택했습니다.
HEIC는 읽기만 되고 쓰기는 안 됩니다. 읽기는 libheif의 WASM 빌드로 로컬에서 처리하지만, 쓰기는 HEVC 인코딩의 특허 문제가 명백한 벽입니다. 쓸 수 있는 인코더를 WASM으로 빌드하면 용량도 큰데, 자주 쓰지도 않는 출력 포맷 하나 때문에 모든 사용자에게 수 MB를 다운로드시키는 건 수지가 안 맞습니다. 이 부분은 서버로 넘기고 UI에도 그렇게 표시합니다.
PDF 압축은 폰트 서브셋팅을 하지 않습니다. 그래서 텍스트 위주 PDF는 절감률이 5~20% 정도에 그칩니다. 이런 파일은 전문 조판 도구가 더 낫고, 이쪽의 주 무대는 스캔 문서와 이미지가 섞인 리포트입니다.
PDF → HTML은 원본보다 커질 수 있습니다. 페이지를 고정 레이아웃 SVG로 재구성해서 텍스트 선택은 되지만, 정적 SVG로 펼치고 나면 원본을 넘을 수 있습니다. 실제 용량을 보여주고 판단하게 만들어 뒀습니다.
덤으로 따라온 두 가지
이미지가 업로드되지 않습니다. 이건 출발점이 아니라 처리를 로컬에 둔 결과로 자연히 따라온 것입니다. 일반적인 포맷의 변환·압축·누끼 과정에서 이미지 업로드 요청이 발생하지 않습니다. DevTools의 Network 패널로 직접 확인할 수 있어요. WASM 코덱과 AI 모델이 내려오는 건 보이지만, 이미지가 나가는 건 안 보입니다. 방향이 반대입니다.
(HEIC 쓰기 같은 일부 전문 포맷은 서버 처리라 업로드가 발생합니다. 그 부분은 UI에 표시해 뒀습니다.)
서버 비용이 들지 않습니다. 처리는 사용자 기기에서 돌고 제 쪽은 정적 파일을 배포할 뿐이라, 1장을 변환하든 1000장을 변환하든 비용이 같습니다. 그래서 횟수 제한이나 워터마크, 회원가입을 붙일 이유가 없습니다. 업로드형 도구가 무료로 풀기 어려운 건 변환 한 번마다 실제 CPU 시간이 나가기 때문이죠.
그래서 왜 하나 더 만들었나
이미지 처리 도구는 확실히 성숙했습니다. 다만 성숙한 건 기능이지 기능에 접근하는 경로가 아닙니다.
포토샵의 능력은 압도적이지만 대가는 설치와 학습입니다. 온라인 AI 도구의 모델도 뒤지지 않지만 대가는 사진을 보내고 기다리는 것입니다. "가끔 십여 장 만지는 사람"에게는 둘 다 수지가 안 맞습니다. 기능이 부족한 게 아니라 그 기능을 쓰기 위해 치러야 하는 값이 너무 비싼 거죠.
처리를 브라우저로 옮기는 건 그 값을 거의 0에 가깝게 만드는 작업이었습니다. 페이지를 여는 것이 설치의 전부이고, 대기열도 업로드도 없습니다. 능력 상한이 브라우저에 묶이는 건 이 선택에 따라오는 대가라서, 위 문단들에 그 경계를 그대로 적어뒀습니다.
여기까지 구현한 결과는 https://imging.ai 에서 돌아갑니다.
