OPFS와 웹 워커로 브라우저에서 초고속·프라이버시 우선 배치 이미지 변환기 만들기
현대 웹 도구들의 문제점
대부분의 온라인 이미지 변환기는 잘못된 흐름을 따릅니다. 파일을 클라우드 서버에 업로드하고, 백엔드에서 처리한 뒤 다시 다운로드하는 것이죠. 수백 장의 이미지를 다룰 경우, 이 구조는 거대한 네트워크 병목을 초래합니다. 더 중요한 것은 사용자의 데이터 프라이버시가 완전히 깨진다는 점입니다.
나는 수백 개의 파일을 즉시 처리하고, 차세대 포맷(WebP, AVIF, QOI)을 지원하며, 사용자 데이터가 로컬 머신을 떠나지 않도록 보장하는 배치 이미지 컨베이어를 만들고 싶었습니다.
하지만 순수히 클라이언트 측에서 구현하려면 두 가지 거대한 과제가 있습니다.
- UI 정지: 이미지 압축은 CPU 집약적인 작업입니다. 메인 스레드에서 실행하면 브라우저가 완전히 응답하지 않게 됩니다.
- 메모리 초과(OOM) 충돌: 100개 이상의 고해상도 이미지에 대한 원시 픽셀 배열을 브라우저의 JavaScript 힙에 보관하면 탭이 즉시 충돌합니다.
현대 Web API를 활용한 아키텍처 해결 방법
아키텍처: 연속 스트리밍 파이프라인
브라우저의 RAM 및 CPU 제한을 우회하기 위해 디커플드 스트리밍 아키텍처를 구축했습니다. Dev.to는 Mermaid.js 렌더링을 기본 지원하므로, 아래와 같이 데스크톱에서 다운로드 폴더까지 데이터가 흐르는 과정을 정확히 보여줍니다.
graph TD
Input[사용자 입력: 드래그 앤 드롭 / 파일] -->|스트림 원시 바이트| OPFS[(OPFS 샌드박스 디스크)]
OPFS -->|순차 읽기| Pool[워커 풀 매니저]
subgraph 다중 스레드 코어 (Backpressure 제한 |Payload 1| W1[워커 1: WebP/Lanczos3]
Pool -->|Payload 2| W2[워커 2: Nearest/QOI]
Pool -->|Payload N| WN[워커 N: AVIF WASM]
end
W1 -->|압축된 Blob| Zip[fflate ZIP 압축기]
W2 -->|압축된 Blob| Zip
WN -->|압축된 Blob| Zip
Zip -->|연속 바이너리 스트림| Download[즉시 로컬 다운로드]
style Multi-Threaded Core fill:#121214,stroke:#39ff14,stroke-width:2px
style OPFS fill:#1f2937,stroke:#58a6ff,stroke-width:1px
style Download fill:#065f46,stroke:#10b981,stroke-width:2px
OPFS(Origin Private File System)로 RAM 팽창 방지
커스텀 웹 워커 풀을 이용한 멀티스레딩
navigator.hardwareConcurrency 를 활용해 사용 가능한 코어 수에 맞춰 워커 풀을 동적으로 구성했습니다.
엄격한 백프레셔 구현
워커 풀에 백프레셔 로직을 넣어, 메모리 사용량이 급증하지 않도록 흐름을 제어했습니다.
스트리밍 ZIP 아카이브 컴파일
fflate 를 실시간으로 사용해 압축된 Blob 스트림을 만들고, 이를 즉시 로컬 다운로드로 연결했습니다.
왜 전체 WebAssembly 단일 파일이 아니었나요?
이와 같은 무거운 유틸리티에 대해 흔히 받는 질문은 “왜 네이티브 C++ 혹은 Rust 이미지 처리 라이브러리를 그대로 WebAssembly(WASM) 바이너리 하나로 컴파일하지 않았나요?” 입니다. WASM은 확실히 빠르지만, 이 아키텍처에 적용하면 다음과 같은 중요한 트레이드오프가 발생합니다.
- 네이티브 브라우저 강점: 현대 브라우저는 WebP 같은 포맷을 위한 하드웨어 가속 렌더링·인코딩 파이프라인을 이미 최적화해 두고 있습니다. 워커 풀 안에서 JS API를 감싸면 거대한 WASM 바이너리 오버헤드 없이 이 네이티브 엔진을 무료로 활용할 수 있습니다.
- OPFS 스레드 동기화: Origin Private File System과 긴밀히 작업하고, 로컬 샌드박스 URL을 생성하며, 런타임 취소를 동적으로 처리하는 일은 비동기 JavaScript/TypeScript 워커를 통해 훨씬 쉽고 안전합니다.
- 하이브리드 접근법: 전체‑WASM 모놀리스를 만들기보다, JavaScript가 오케스트레이션·파이프라인 상태·네이티브 코덱을 담당하고, WASM은 JS가 너무 느린 부분(예: 무거운 Lanczos3 이미지 리샘플링(Pica) 및 고급 AVIF 인코딩)만을 백그라운드 워커에서 사용하도록 제한했습니다.
내부 기술 스택
- Vite + TypeScript: 빠른 빌드와 타입 안전성을 제공하는 코어 파이프라인
- Pica: 산업용 수준 Lanczos3 리샘플링 필터 (WASM 가속)
- @jsquash/avif: 멀티스레드, 산업용 AVIF 인코딩 알고리즘 (WASM 컴파일)
- fflate: 고속·메모리 효율적인 바이너리 ZIP 압축
- StreamSaver: 낮은 오버헤드의 클라이언트‑사이드 스트리밍 다운로드
성능 벤치마크
8코어 머신에서 파이프라인을 테스트한 결과, 브라우저 스토리지가 네이티브 데스크톱 도구와 경쟁할 수 있음을 입증했습니다.
- 100장 고해상도 이미지(각 10 MB): ≈ 45초
- 500장 모바일 에셋(각 2 MB): ≈ 2분
- 최대 RAM 사용량: 200 MB 이하 유지
- 디스크 사용량: OPFS에 직접 스트리밍, 메모리 누수 없음
확인해 보세요!
이 프로젝트는 완전 무료이며 오픈소스이고, 트래커·쿠키·광고가 전혀 없습니다.
- 실제 앱: https://sapianyi.github.io/PixelForge-Mass/
- 소스 코드: https://github.com/Sapianyi/PixelForge-Mass
아키텍처에 대한 여러분의 생각을 듣고 싶습니다! 클라이언트 측에서 무거운 에셋을 어떻게 다루고 계신가요? 댓글로 자유롭게 토론해 주세요!