아이폰에서 웹 배경음이 안 나던 이유 — WKWebView와 Web Audio
스크롤을 내리면 잠수함이 바다 깊은 곳으로 가라앉는 작은 웹게임을 만들어 인터넷에 올렸습니다. 빌드 도구 없이 HTML·CSS·자바스크립트만으로 만들었고, 배경음마저 음원 파일 없이 Web Audio API로 직접 합성했습니다(웹 기초로 웹게임 만들기에서 다룬 그 프로젝트입니다). 데스크톱에서는 소리 버튼을 누르면 배경음이 잘 흘렀습니다. 그런데 아이폰에서 열어 보니 같은 버튼을 눌러도 아무 소리가 나지 않았고, 여기서부터 한참을 직접 파고들어야 했습니다. 이 글은 그 트러블슈팅 기록입니다.
먼저 증상을 정확히 적어 봤습니다
막연히 "안 된다"로 두면 손댈 곳을 못 잡겠어서, 가장 먼저 증상을 최대한 구체적으로 적어 봤습니다.
- 🔇 → 🔊 버튼을 누르면 표시는 바뀐다 (= 클릭 핸들러는 실행됨)
- 그런데 배경음은 들리지 않는다
- 같은 아이폰에서 유튜브·다른 사이트의 소리는 정상
- 무음 스위치는 껐고, 볼륨도 올렸고, 저전력 모드도 아니다
버튼 표시가 바뀐다는 건 소리를 켜는 코드가 분명히 돌았다는 뜻이었습니다. 그래서 저는 문제를 "소리를 만드는 단계"가 아니라 "만들어진 소리가 스피커로 나가는 단계"로 좁혀 잡고 출발했습니다.
가설을 하나씩 세우고 직접 지워 나갔습니다
가장 먼저 아이폰 측면의 무음 스위치를 의심했습니다. iOS에서는 무음 모드일 때 Web Audio 소리가 묻히는 경우가 있다고 알고 있었기 때문입니다. 직접 스위치를 끄고 볼륨을 올려 다시 눌러 봤지만 그대로 조용했습니다. 첫 번째 가설은 접었습니다.
다음으로 카카오톡 같은 인앱 브라우저를 의심했습니다. 앱 안에서 열리는 브라우저는 Web Audio를 막는 일이 잦으니까요. 그래서 같은 주소를 모바일 크롬에서 직접 열어 봤는데, 거기서도 무음이었습니다. 두 번째 가설도 접었습니다. 그런데 바로 그 결과가 결정적인 힌트가 됐습니다.
iOS에서는 모든 브라우저가 애플의 WebKit 엔진을 쓰도록 강제됩니다. 아이폰의 크롬조차 속은 사파리와 같은 WebKit입니다.
그러니 "크롬에서도 안 된다"는 건 WebKit을 벗어났다는 뜻이 아니라, 오히려 어떤 브라우저를 열어도 같은 엔진 위에 있다는 뜻이었습니다. 인앱 브라우저 탓이 아니라 WebKit 자체의 무언가라고 보고, 저는 그 전제로 다시 출발했습니다.
증상을 다시 들여다보고 원인을 추렸습니다
제가 붙잡은 단서는 "다른 영상은 소리가 나는데 이 게임만 무음"이라는 점이었습니다. 영상이나 오디오 플레이어는 <audio>·<video> 같은 미디어 요소(HTMLMediaElement)를 씁니다. 반면 제 게임의 배경음은 음원 파일이 한 개도 없이, Web Audio API의 오실레이터를 코드로 합성해 냅니다. 둘의 차이가 곧 답일 것 같았습니다.
그래서 한 가지 가설을 세웠습니다. WKWebView에서는 미디어 요소가 한 번 재생되어 오디오 세션이 열리기 전까지는, Web Audio의 출력이 스피커로 나가지 않는 게 아닐까 하는 것이었습니다. 다른 사이트는 진짜 미디어 요소를 재생해 세션이 이미 열려 있었고, 제 게임은 순수 Web Audio라 세션을 열어 줄 미디어 요소가 아예 없었던 셈입니다. 이 가설은 뒤에서 직접 무음 audio를 재생해 보고 사실로 확인됩니다.
표준 해법을 먼저 넣어 봤지만 제 경우엔 부족했습니다
"iOS에서 Web Audio가 안 들린다"를 검색하면 거의 같은 처방이 나옵니다. 사용자가 화면을 누르는 그 순간(제스처) 안에서 AudioContext.resume()을 부르고, 무음 버퍼를 한 번 재생해 오디오를 '언락'하라는 것입니다. 그래서 저도 이 표준 언락부터 코드에 넣어 봤습니다.
// 흔히 권하는 언락 — 사파리에서는 통합니다
ctx.resume();
const src = ctx.createBufferSource();
src.buffer = ctx.createBuffer(1, 1, ctx.sampleRate);
src.connect(ctx.destination);
src.start(0);
애석하게도 아이폰에서는 여전히 무음이었습니다. 왜 안 됐는지 직접 따져 보니 이유가 있었습니다. 저 무음 버퍼조차 결국 Web Audio를 통해 재생됩니다. 막혀 있는 바로 그 경로로 소리를 내보내는 셈이라, 정작 길을 열어 줘야 할 미디어 요소는 한 번도 등장하지 않았던 것입니다. 진짜 사파리에서는 이 정도로도 충분했지만, 아이폰 크롬(WKWebView)에서는 부족했습니다.
무음 audio로 오디오 세션을 먼저 여는 쪽으로 고쳤습니다
방향은 분명해졌으니 코드를 바꿔 봤습니다. 막힌 Web Audio가 아니라 진짜 미디어 요소를 한 번 재생해 오디오 세션을 먼저 열어 주기로 했습니다. 소리 버튼을 누르는 제스처 안에서, 들리지 않는 짧은 <audio>를 한 번 재생하도록 했습니다.
// 미디어 요소를 한 번 재생해 iOS 오디오 세션을 연다
if (!silentEl) {
silentEl = document.createElement("audio");
silentEl.setAttribute("playsinline", "");
silentEl.loop = true;
silentEl.src = SILENT_WAV; // 코드로 만든 무음 WAV (외부 파일 0)
}
silentEl.play().catch(() => {}); // 자동재생이 거부돼도 안전하게
이 미디어 요소가 세션을 열고 나자, 그 뒤로는 Web Audio 오실레이터 소리가 정상적으로 스피커로 흘러나왔습니다. 가설이 맞았던 것입니다. 한 가지 더 신경 쓴 부분은 무음 음원이었습니다. 이 프로젝트는 "음원 파일 0개"를 원칙으로 삼았기에, 무음 WAV조차 외부 파일을 두지 않고 DataView로 WAV 헤더를 직접 채워 data URI로 코드 생성했습니다. 더불어 컨텍스트가 깨어난(running) 뒤에 음악이 확실히 시작되도록, resume()의 완료를 기다렸다가 한 번 더 시작을 보장하게 했습니다.
아이폰에서 직접 확인하고, 남은 생각
데스크톱은 기존과 똑같이 동작하는지부터 확인했습니다(AudioContext 상태가 running으로 정상). 아이폰은 개발자 콘솔을 붙이기가 번거로워, 화면에 오디오 상태를 잠깐 띄우는 임시 코드까지 직접 넣어 "컨텍스트는 running인데도 무음"임을 눈으로 확인한 뒤 원인을 좁혔고, 확인이 끝난 다음 그 코드는 걷어 냈습니다. 그리고 실기기에서 마침내 배경음이 흐르는 것을 들었습니다.
돌이켜 보면 배운 것은 세 가지였습니다. 먼저, "아이폰은 모든 브라우저가 WebKit"이라는 사실은 머리로는 알고 있었어도 이렇게 직접 발에 걸려 봐야 비로소 체감된다는 것입니다. 다음으로, 검색하면 나오는 표준 해법이 늘 내 환경까지 덮어 주지는 않는다는 것입니다. 사파리에선 되는 코드가, 같은 엔진을 쓴다는 크롬에선 부족했으니까요. 마지막으로, 결국 길을 열어 준 것은 영리한 묘수가 아니라 "미디어는 나는데 Web Audio만 무음"이라는 증상을 끝까지 들여다본 것이었다는 생각이 들었습니다. iOS의 오디오·설치 제약을 다룬 일은 전에도 있었는데(PWA로 admin 페이지에 모바일 푸시 알림 붙이기), 플랫폼의 작은 규칙 하나가 사용자 경험을 통째로 가른다는 점은 매번 비슷하게 다가옵니다.
혹시 같은 증상으로 헤매는 분이 있다면, 먼저 AudioContext가 running 상태인지 확인하고, running인데도 조용하다면 무음 <audio>로 오디오 세션을 먼저 열어 보시길 권합니다. 직접 고쳐 완성한 결과물은 여기에서 들어 볼 수 있습니다.
관련 글
PWA로 admin 페이지에 모바일 푸시 알림 붙이기
알림 하나 받자고 네이티브 앱을 만드는 건 오버킬이었습니다. 그래서 이미 쓰던 admin 웹앱에 PWA를 최소한으로 얹어 모바일 푸시 알림만 받기로 했습니다. iOS 설치형 제약과 푸시 전용 service worker, Slack fan-out까지 직접 부딪쳐 얻은 교훈을 담았습니다.
공개된 정보만으로 웹사이트 기술스택 추론하기 — F12와 curl이면 충분합니다
곧 손대야 할 사이트의 코드를 받기 전, 브라우저 개발자도구와 curl만으로 프론트·백엔드 기술스택을 가늠해본 과정입니다. 프레임워크 시그니처와 응답 헤더·쿠키 단서 읽기, 'next' 단어에 헛짚은 사례, 추정과 확정을 등급으로 가르는 정직함까지 담았습니다.
Vrew 내보내기가 0%에서 멈출 때 — 로그가 알려준 진짜 원인
Vrew 내보내기가 특정 프로젝트만 0%에서 멈췄습니다. 처음엔 투명 GIF의 알파 채널이 원인이라 확신했지만, 과거 성공 로그에도 투명 GIF가 멀쩡히 들어 있었습니다. 가설을 버리고 성공·실패 GIF를 나란히 비교하니 진짜 차이는 길이였습니다. 안 되는 사례만이 아니라 되는 사례를 함께 봐야 진짜 변수가 드러난다는 것을, 로그를 따라가며 다시 배운 기록입니다.