Sangcho.log
dev

JS/WASM, 그리고 WebCodecs — 웹 미디어 처리의 삼분할 지형도

8 min read|

"컨테이너 파싱은 JS/WASM 라이브러리에 맡긴다"는 말에서 시작합니다. JS와 WASM, 두 개의 서로 다른 실행 방식을 슬래시로 묶어놓은 표현인데, 왜 하필 이 둘이 데뮤싱 라이브러리의 선택지가 되는지가 핵심입니다.

1. 두 단어의 정체

  • JS (JavaScript): 브라우저가 원래부터 실행하는 스크립트 언어. 우리가 평소 짜는 그 코드입니다.
  • WASM (WebAssembly): 브라우저에서 돌아가는 저수준 바이너리 포맷. C/C++/Rust 같은 언어로 짠 코드를 컴파일해서 브라우저 안에서 거의 네이티브에 가까운 속도로 실행할 수 있게 해줍니다.

즉 "JS/WASM 라이브러리"란 자바스크립트로 짜였거나, WebAssembly로 컴파일되어 돌아가는 라이브러리를 뭉뚱그린 표현입니다. 데뮤싱은 브라우저 네이티브 API가 없으니 이 두 방식 중 하나로 구현할 수밖에 없다는 뜻입니다.

2. WebAssembly가 왜 존재하나

JavaScript는 편하지만, 바이트 단위로 파일을 파싱하거나 무거운 미디어 처리를 하기엔 느립니다. WebAssembly는 그 틈을 메우려고 나왔습니다.

  • 속도 — 이미 검증된 C/C++ 라이브러리(예: libav/FFmpeg의 내부 로직)를 그대로 컴파일해서 브라우저에 가져올 수 있습니다. 매번 JS로 새로 짤 필요가 없습니다.
  • 이식성 — FFmpeg처럼 데스크톱에서 수십 년 검증된 코드를 웹으로 옮길 때, WASM으로 컴파일하면 재작성 없이 브라우저에서 돌릴 수 있습니다.

3. 데뮤싱 맥락에서 이게 왜 중요한가

컨테이너(MP4/WebM)의 박스 구조를 파싱하는 건 바이트 오프셋을 훑고 스펙을 따라가는 작업이라, 원래 이런 일을 잘하는 C 라이브러리(FFmpeg 계열)를 WASM으로 가져오는 접근이 자연스럽습니다.

라이브러리들이 실제로 두 갈래로 나뉘는 이유가 여기 있습니다.

  • 순수 JS 데뮤서 (mp4box.js, @remotion/media-parser): 컨테이너 파싱만 하면 되는 경우. 파싱은 상대적으로 가벼운 작업이라 JS로 충분하고, 번들이 작고 다루기 쉽습니다.
  • WASM 기반 (FFmpeg.wasm, libav.js): 파싱을 넘어 실제 인코딩/디코딩/필터까지 다 하려는 경우. 무거운 연산이라 WASM의 속도가 필요합니다.

4. WASM의 함정 — 왜 항상 WASM은 아닌가

여기가 실무 판단의 핵심입니다. FFmpeg.wasm은 인기 있는 비디오 처리 도구 FFmpeg를 브라우저에서 돌리는 포트인데, 대가가 있습니다.

  • 하드웨어 가속 부재 — WASM은 브라우저의 전용 코덱 하드웨어에 직접 접근하지 못합니다. 반면 WebCodecs는 이미 브라우저 안에 있는 특화된 하드웨어에 직접 접근해서 데스크톱 앱만큼 성능이 나옵니다.
  • 번들·메모리 부담 — FFmpeg 전체를 WASM으로 가져오면 다운로드 용량이 크고, 메모리 상한도 생깁니다. 100MB보다 큰 비디오를 다루기 어렵다는 제한이 여기서 나옵니다.

그래서 요즘의 이상적인 구조는 역할을 쪼개는 것입니다. 무거운 인코딩/디코딩은 하드웨어 가속되는 WebCodecs에 맡기고, WASM/JS 라이브러리는 그게 못 하는 컨테이너 파싱(demux/mux) 부분만 담당하게 하는 거죠. WebCodecs가 나오기 전엔 어쩔 수 없이 FFmpeg.wasm으로 다 처리했지만, 지금은 그 조합이 더 빠르고 가볍습니다.

5. 전체 파이프라인 — 두 계층의 릴레이

위쪽은 재생(읽기) 흐름, 아래쪽은 내보내기(쓰기) 흐름입니다.

[재생 — 상자를 벗기고, 압축을 푼다]

 파일(MP4/WebM)                                          캔버스
   │                                                       ▲
   ▼                                                       │
 ┌──────────┐   EncodedVideoChunk   ┌──────────┐   VideoFrame
 │  Demux   │ ────────────────────▶ │  Decode  │ ──────────┘
 └──────────┘                       └──────────┘
  JS/WASM 라이브러리                  WebCodecs (HW 가속)


[내보내기 — 정확히 역순]

 편집된 VideoFrame                                     새 파일
   │                                                       ▲
   ▼                                                       │
 ┌──────────┐   EncodedVideoChunk   ┌──────────┐          │
 │  Encode  │ ────────────────────▶ │   Mux    │ ──────────┘
 └──────────┘                       └──────────┘
  WebCodecs (HW 가속)                 JS/WASM 라이브러리

재생은 왼쪽에서 오른쪽으로, 상자를 벗기고(Demux) 압축을 푸는(Decode) 순서입니다. Demux가 컨테이너를 열어 EncodedVideoChunk를 뽑아내면, Decode가 그 청크를 VideoFrame으로 풀어 캔버스에 그립니다. 스크러버 미리보기가 바로 이 흐름을 탑니다.

내보내기는 정확히 역순입니다. 편집된 VideoFrame을 Encode로 다시 압축해 청크로 만들고, Mux가 그 청크들을 컨테이너 규격에 맞게 새 파일로 포장합니다.

경계선이 핵심입니다. Demux/Mux는 브라우저에 네이티브 API가 없어서 JS/WASM 라이브러리(Mediabunny, mp4box.js 등)가 담당하고, Decode/Encode는 하드웨어 가속되는 WebCodecs가 담당합니다. 두 계층이 릴레이하듯 바통을 주고받는 구조이고, "컨테이너 작업은 라이브러리, 코덱 작업은 WebCodecs"라는 경계선은 재생이든 내보내기든 그대로 유지되며 방향만 뒤집힙니다.

더 깊이: 100MB 제한은 어디서 오나

FFmpeg.wasm의 파일 크기 제한은 마법의 숫자가 아니라 구조에서 옵니다. 클래식한 WASM은 32비트 메모리 모델이라 한 인스턴스가 다룰 수 있는 메모리에 상한(이론상 4GB, 실제로는 브라우저 정책상 훨씬 작게)이 있고, FFmpeg.wasm은 가상 파일시스템(MEMFS)에 입력 파일 전체를 메모리로 올려놓고 작업하기 때문입니다. 입력 + 중간 버퍼 + 출력이 전부 메모리에 공존하니 실질 한계가 확 줄어듭니다.

우회 전략의 방향은 하나입니다 — 전체를 올리지 말고 흘려보내기. 데뮤서가 파일을 청크 단위로 읽고(File.slice() + 스트리밍 파싱), 디코딩·인코딩도 청크 단위로 릴레이하면 메모리 사용량은 "파일 크기"가 아니라 "동시에 물고 있는 청크 수"에 비례하게 됩니다.

더 깊이: 무거운 절반은 나중에 내려받기

재생만 필요한 화면(미리보기)에서는 파이프라인의 위쪽 절반(Demux → Decode)만 있으면 됩니다. Encode/Mux 코드는 "내보내기" 버튼을 누르기 전까지 필요 없으니, dynamic import로 분리하면 초기 로딩이 가벼워집니다.

// 내보내기 버튼을 눌렀을 때에야 무거운 쪽을 로드
async function handleExport() {
  const { createMuxer } = await import('./export-pipeline'); // 별도 청크로 분리됨
  ...
}

WASM 바이너리도 마찬가지로 필요 시점에 fetch + instantiateStreaming으로 늦게 받는 것이 정석입니다.

더 깊이: 코덱 폴백은 물어보고 시작하기

데뮤서가 알려준 코덱 문자열(avc1.64001f 같은)을 디코더에 그대로 넘기기 전에, WebCodecs는 지원 여부를 미리 물어보는 API를 줍니다.

const support = await VideoDecoder.isConfigSupported({ codec: 'avc1.64001f' });
if (!support.supported) {
  // 폴백 순서: ① 다른 코덱 프로파일 시도 → ② WASM 디코더로 강등 → ③ 서버 트랜스코딩 안내
}

"일단 만들고 에러 나면 잡는다"보다 훨씬 깔끔하고, 폴백 사다리(WebCodecs → WASM → 서버)를 명시적으로 설계할 수 있습니다.

더 깊이: 그럼 오디오는?

같은 지형도가 오디오에도 그대로 적용됩니다. 디코딩은 WebCodecs의 AudioDecoder(결과물은 AudioData)가 담당하고, 재생·믹싱·이펙트는 Web Audio API(AudioContext, AudioWorklet)의 영역입니다. 즉 Web Audio는 "스피커로 나가기 직전의 가공·재생 계층"이고, 그 앞단의 압축 해제는 WebCodecs가 맡는 관계입니다.


한 줄 요약 — 파싱만 필요하면 가벼운 JS 데뮤서, 무거운 처리까지 필요하면 WASM, 하드웨어 가속이 되는 코덱 작업은 WebCodecs. 이 삼분할이 현재 웹 미디어 처리의 기본 지형도입니다.

스스로 점검할 질문

  1. FFmpeg.wasm처럼 무거운 WASM 라이브러리를 써야만 하는 상황과, 가벼운 JS 데뮤서 + WebCodecs 조합으로 충분한 상황을 가르는 구체적인 판단 기준은 뭘까?
  2. 트랜스코딩(Demux → Decode → Encode → Mux)을 브라우저에서 전부 돌릴 때, 네 단계의 처리 속도가 제각각이라 병목이 생긴다면 각 단계 사이 큐를 어떻게 throttle해야 메모리가 안 터질까?
  3. Studio/Dubbing에서 WASM 라이브러리를 도입한다면, 초기 다운로드 용량 부담을 줄이기 위한 지연 로딩·코드 스플리팅은 어떤 식으로 적용하는 게 좋을까?

참고 링크