메인 스레드가 멈췄습니다. 사용자가 4K 영상을 업로드하자마자 브라우저 탭이 '응답 없음' 상태로 빠졌고, 크롬 작업 관리자의 메모리 사용량은 2GB를 넘어섰습니다. 단순한 Canvas API와 JavaScript 루프만으로는 고해상도 프레임 단위 조작이 불가능했습니다. 서버 리소스를 아끼기 위해 클라이언트 사이드 프로세싱을 도입하려던 계획이 수포로 돌아갈 위기였습니다. 이 글은 JavaScript의 싱글 스레드 병목을 해결하기 위해 Rust와 WebAssembly(WASM)를 도입하여 프로덕션 환경에서 비디오 인코딩 파이프라인을 구축한 기록입니다.
왜 JavaScript만으로는 안 되는가 (Root Cause)
JavaScript 엔진(V8)은 훌륭하지만, 이미지/비디오 처리에 있어서는 명확한 한계가 존재합니다. Garbage Collection(GC)에 의한 'Stop-the-world' 현상은 실시간 렌더링에서 프레임 드랍을 유발하며, 동적 타이핑으로 인한 오버헤드는 픽셀 단위 연산에서 치명적입니다.
우리가 겪은 문제는 CPU 집약적인 인코딩 작업이 UI 스레드를 차단한다는 점이었습니다. Web Worker를 써도 데이터 직렬화(Serialization) 비용이 만만치 않습니다. 여기서 Rust 웹 개발 생태계가 빛을 발합니다. Rust의 소유권 모델과 무비용 추상화는 메모리 안전성을 보장하면서도 C++ 수준의 성능을 냅니다. 이를 WebAssembly로 컴파일하면 네이티브에 가까운 속도로 브라우저 위에서 바이너리를 실행할 수 있습니다.
WASM에서
SharedArrayBuffer를 사용하여 멀티스레딩 성능을 극대화하려면 서버 응답 헤더에 Cross-Origin-Opener-Policy: same-origin과 Cross-Origin-Embedder-Policy: require-corp를 반드시 설정해야 합니다. 보안 정책 미준수 시 브라우저가 버퍼 할당을 차단합니다.
솔루션: FFmpeg WASM 구현
직접 코덱을 구현하는 것은 비효율적입니다. 업계 표준인 FFmpeg를 브라우저로 포팅한 FFmpeg WASM 라이브러리를 사용했습니다. 핵심은 파일 시스템 가상화(MEMFS)를 통해 브라우저 메모리 내에서 입출력을 처리하는 것입니다.
다음은 사용자가 업로드한 비디오 파일을 메모리 FS에 쓰고, WASM 모듈을 통해 mp4로 트랜스코딩한 뒤 다시 JS로 가져오는 코드입니다.
// FFmpeg 로드 및 초기화 (비동기 처리 필수)
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
// 로깅 활성화로 디버깅 용이성 확보
const ffmpeg = createFFmpeg({ log: true });
const transcodeVideo = async (file) => {
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
// 1. JS File 객체를 WASM의 가상 파일 시스템(MEMFS)에 쓰기
// fetchFile은 Blob/File 객체를 Uint8Array로 변환해줍니다.
ffmpeg.FS('writeFile', 'input.avi', await fetchFile(file));
// 2. FFmpeg 명령어 실행
// -i: 입력 파일, output.mp4: 출력 파일
// libx264 코덱을 사용하여 H.264로 인코딩
console.time('WASM_Encoding');
await ffmpeg.run('-i', 'input.avi', '-c:v', 'libx264', 'output.mp4');
console.timeEnd('WASM_Encoding');
// 3. 결과 파일 읽기 (Uint8Array 반환)
const data = ffmpeg.FS('readFile', 'output.mp4');
// 4. 브라우저에서 다운로드 가능한 URL 생성
const url = URL.createObjectURL(
new Blob([data.buffer], { type: 'video/mp4' })
);
// 메모리 누수 방지를 위해 가상 파일 삭제
ffmpeg.FS('unlink', 'input.avi');
ffmpeg.FS('unlink', 'output.mp4');
return url;
};
성능 검증 (WASM 성능 vs JS)
단순한 이미지 필터링 작업과 비디오 인코딩 작업에서 순수 JavaScript와 WebAssembly의 성능 차이는 압도적이었습니다. 특히 SIMD(Single Instruction, Multiple Data) 지원이 활성화된 환경에서 WASM 성능은 네이티브 앱의 약 80~90% 수준까지 도달했습니다.
| 작업 유형 (1080p 기준) | JavaScript (Canvas API) | WebAssembly (Rust/FFmpeg) | 성능 향상 |
|---|---|---|---|
| Grayscale 변환 | 120ms | 15ms | 8x |
| Gaussian Blur | 450ms | 42ms | 10x |
| H.264 인코딩 (5초) | 불가능 (OOM 발생) | 3.2s | 성공 |
RUSTFLAGS="-C target-feature=+simd128" 옵션을 주면 벡터 연산 최적화가 적용되어 이미지 처리 속도가 2배 이상 빨라질 수 있습니다.
Conclusion
서버 비용 절감과 UX 개선이라는 두 마리 토끼를 잡으려면 무거운 연산은 클라이언트로 내려야 합니다. 하지만 JavaScript만으로는 한계가 명확합니다. Rust와 FFmpeg를 결합한 WebAssembly 파이프라인은 더 이상 실험적인 기술이 아닙니다. 비디오 편집기, 화상 회의 백그라운드 블러 처리 등 고성능이 요구되는 웹 애플리케이션에서 WASM은 선택이 아닌 필수 생존 도구입니다. 지금 바로 프로덕션 코드의 병목 구간을 프로파일링하고, WASM 모듈로 교체를 검토해 보십시오.
Post a Comment