모바일 WebView에서 video 태그를 다루며 겪은 것들
2026-04-15모든 것의 시작: 20MB 영상
최근 영상 섹션을 개발해야 했습니다.(이하 모먼트)
모먼트는 최대 10개의 영상을 보여주었고 인스타그램 스토리와 유사한 UI를 구성해야 했죠.
처음 모먼트 기능이 도입됐을 때 영상 크기 제한은 20MB였습니다.
20MB는 상당히 큰 용량이라 부담스러웠지만 커머스팀에서 최대한 줄일 수 있는 영상 크기가 20MB라고 전달을 받았으며, 기능 자체를 빠르게 출시하는 게 우선이라 영상 용량 정책은 "일단 넉넉하게" 잡고 시작했습니다. 그런데 iOS WebView 환경에서 20MB 영상 여러 개를 동시에 핸들링하는 건 생각보다 훨씬 가혹한 조건이었습니다. 실제로 사용자 리포트와 내부 테스트에서 앱 크래시와 기기 발열 문제가 반복해서 올라오기 시작했습니다.
임시방편: 직접 압축 도구를 만들다
근본적인 해결은 서버 사이드 영상 압축 파이프라인 구축이었지만, 인프라 작업에는 시간이 필요했습니다. 급한 불을 끄기 위해 직접 영상 압축 웹 도구를 만들어 임시 운영했습니다.
video-encoder — 브라우저에서 바로 영상을 압축할 수 있는 임시 도구
콘텐츠 업로드 전에 이 도구를 거쳐 압축한 영상만 등록하는 방식으로 운영했습니다. 물론 이건 영구적인 해결책이 아니었습니다. 업로더 입장에서 추가 단계가 생기고, 압축 품질도 일정하지 않았습니다.
하지만 이 임시 조치 덕분에 한 가지는 명확해졌습니다. 영상을 5MB 이하로 줄이면 크래시와 발열 문제가 눈에 띄게 줄어든다는 것이었습니다.
WebView 환경의 한계
본격적인 개선 이야기 전에 iOS WebView라는 환경 자체의 제약을 짚어둘 필요가 있습니다.
디코더 수 제한: iOS WebView는 동시에 활성화할 수 있는 비디오 디코더 수에 제약이 있습니다. 여러 개의 <video> 태그를 동시에 재생 상태로 두면 메모리 경고가 발생하거나 디코더가 강제로 해제됩니다.
autoplay 정책: iOS Safari 기반 WebView는 사용자 인터랙션 없이 자동 재생이 제한됩니다. muted 속성을 붙여도 WebView 설정에 따라 동작이 달라지는 경우가 있습니다.
이벤트 신뢰도: canplay, loadeddata 같은 미디어 이벤트가 데스크탑 브라우저와 달리 iOS WebView에서는 발화 타이밍이 불규칙하거나 아예 발화하지 않는 경우가 있습니다.
메모리 압박: 백그라운드 전환이나 앱 스위칭 시 WebView가 메모리를 회수하면서 비디오 버퍼가 날아가는 경우가 있습니다. 복귀 후 재생이 처음부터 다시 시작되는 현상이 여기서 비롯됩니다.
이런 환경에서 "인스타그램 스토리처럼 부드러운 전환"을 만드는 건 단순한 UX 문제가 아니라 플랫폼의 물리적 제약과 싸우는 일이었습니다.
반복된 개선의 역사: 단일 video 태그의 한계
초기 구현은 단순했습니다. <video> 태그 하나를 두고, 영상이 넘어갈 때마다 src를 해제하고 다시 세팅하는 방식이었습니다.
// 초기 구현 — 단순하지만 문제가 있었다
videoRef.current.src = "";
videoRef.current.load();
videoRef.current.src = nextVideoUrl;
videoRef.current.play();
src를 비우는 순간 화면이 검게 되고, 새 영상이 로드되기까지 그 공백이 유지됩니다. 네트워크 상태에 따라 이 공백이 길어질 수 있었고, 빠르게 영상을 넘길수록 깜빡임이 쌓였습니다.
그렇다면 네이티브로 가는 게 정답이었을까?
깜빡임 문제가 반복되면서 자연스럽게 나온 질문이 있었습니다. "그냥 이 화면을 네이티브로 구현하면 되지 않나?"
기술적으로는 맞는 말입니다. iOS의 AVQueuePlayer는 영상을 큐에 미리 올려두고 전환 시 즉시 스왑하기 때문에 black frame이 구조적으로 발생하지 않습니다. 디코더 제어도 WebView보다 훨씬 자유롭습니다.
하지만 모먼트 화면만 네이티브로 분리하기엔 현재 회사 사정에 맞지 않았습니다.
결론적으로 네이티브는 기술적으로는 더 유리하지만, 현실적인 제약으로 네이티브 전환은 콘텐츠 복잡도가 올라가거나 WebView의 한계를 명확히 넘어서는 시점에 재검토하는 게 맞다고 판단했습니다.
Dual Buffer 전환 엔진
이번에는 탭형 UI 구조는 유지하면서 전환 엔진만 교체했습니다.
Dual Buffer 구조
primary / secondary 두 개의 video를 두고, standby 슬롯에서 다음 영상을 미리 준비합니다. 준비가 완료된 시점에 active 슬롯을 스왑하기 때문에 black frame 없이 전환이 가능합니다.
primary (active) → 현재 재생 중인 슬롯
secondary (standby) → 다음 영상 사전 준비
ready → swap → 준비 완료 시 active 슬롯 교체
전환 준비 조건 완화
loadeddata만 기다리는 대신, loadedmetadata, canplay, loadeddata 중 먼저 도착하는 이벤트로 준비 완료를 처리합니다. timeout 시에도 readyState >= 1이면 진행해 네트워크 조건에 관계없이 멈추지 않습니다.
빠른 연타 대응
전환 중 요청은 pending으로 1개만 유지하고, 다음 인덱스 계산은 현재 인덱스가 아닌 pending || current 기준으로 수행합니다.
// 연타 시 pending을 기준으로 다음 인덱스를 계산
const nextIndex = getNextIndex(pendingIndex ?? currentIndex, direction);
race condition 방지
전환 시작 시 이전 cleanup timer를 즉시 해제하고, cleanup 실행 시 "지금 stale 슬롯이 맞는지" 검증 후 release합니다.
pause 상태 동기화
showPauseIcon을 ref로 동기화하고, 좌/우 탭 시 ref를 먼저 false로 반영한 뒤 전환합니다. "멈춘 영상에서 다음으로 넘겼더니 다음 영상도 멈춤" 버그를 이걸로 제거했습니다.
롤백 안전장치
기존 싱글 버퍼 전환 코드는 삭제하지 않고 주석으로 파일 내 보존했습니다. 새 엔진에서 문제가 생기면 주석 해제만으로 빠르게 복구할 수 있습니다.
왜 HLS는 아니었나
HLS도 후보였지만, 이번 문제의 최적해는 아니었습니다.
콘텐츠 특성: 모먼트는 짧은 클립, 각 영상 5MB 제한입니다. 긴 재생 구간에서 adaptive bitrate 이점이 큰 HLS의 장점이 상대적으로 작습니다.
문제의 본질: 이번 이슈는 "비트레이트"가 아니라 "아이템 간 전환 타이밍/상태관리" 문제였습니다. HLS로 바꿔도 전환 경합과 첫 프레임 타이밍 문제는 여전히 해결이 필요합니다.
운영 복잡도: HLS 도입 시 인코딩 파이프라인, manifest 관리, CDN 정책 등 백엔드/인프라 작업이 큽니다.
iOS WebView 현실: iOS에서 중요한 건 동시 활성 디코더 수를 낮게 유지하는 것입니다. Dual Buffer는 디코더를 2개로 고정하면서 서버 구조 변경 없이 리스크를 통제할 수 있습니다.
이번 단계는 HLS 도입보다 전환 파이프라인 개선이 ROI가 높았습니다.
결과와 교훈
정량 지표는 추가 수집 중이지만, 구조적으로 해결된 것들은 명확합니다.
- 전환 시 black frame 감소
- 빠른 연타 시 멈춤 현상 완화
- pause/next 상태 불일치 버그 제거
- 롤백 안전장치 확보
물론 이번 개선이 완벽한 해답은 아닙니다. 아직 다듬어야 할 부분도 남아있고, 환경이 달라지면 전혀 다른 선택이 맞을 수도 있습니다. 그래도 여러 트레이드오프를 직접 겪으며 결정을 내리는 과정에서, 미처 몰랐던 개념들을 하나씩 알아가는 것 자체가 값진 경험이었습니다.