크롬은 되는데 웨일 브라우저에서만 스크립트가 안 돌 때 — Cannot destructure property of undefined
각자 폰에 준비한 안무 영상을 리더의 조작에 맞춰 재생하는 무용 연습용 영상 동기화 웹앱을 만들고 있었다. 노트북에 서버를 매번 띄우는 번거로움을 줄이려고 동기화 신호를 클라우드로 옮긴 뒤, 안드로이드 웨일에서 영상을 골라도 방 만들기와 참여 버튼이 켜지지 않았다. 같은 폰의 일반 크롬에서는 동작했다.
결론
이번 웨일(안드로이드) 장애는 fingerprint.js를 mediacheck.js로 바꾸고 두 페이지의 참조를 수정한 뒤 해결됐다. 배포 후 정상 동작했다는 응답이 있었다.
직접 확인된 오류는 페이지 진입 직후 window.FormationFingerprint가 undefined여서 fingerprintVideo를 구조분해하는 코드가 중단된 것이다. 당시 분석은 이 초기화 실패 때문에 파일 이벤트 처리와 WebSocket 연결 등 뒤의 코드가 실행되지 않았다는 것이었다.
아래는 수정 방향을 보여주는 예시다. 기록에 태그 변경 내역 전체가 남아 있지 않아 실제 코드를 그대로 옮긴 것은 아니다.
<!-- 변경 전 -->
<script src="fingerprint.js"></script>
<!-- 변경 후 -->
<script src="mediacheck.js"></script>
파일명 변경 후 해결됐다는 관찰과, 추적 방지가 파일명을 차단했다는 설명은 구분해야 한다. 추적 방지 설명은 당시 분석에서 제시한 추론이다. 어떤 기능이나 필터가 차단했는지 보여주는 기록은 없다. 변경 후 데스크톱에서는 FormationFingerprint 객체와 함수가 확인됐다.
같은 오류를 조사할 때는 파일 선택 처리보다 먼저 페이지 최초 오류와 전역 객체 생성 여부를 확인하자. 이번에는 ?debug=1 화면 패널에 표시된 페이지 진입 오류가 진단의 전환점이었다.
상황
리더가 연습방을 만들고 영상을 선택하면, 참여자는 방 목록에서 자기 방에 들어가 각자 준비한 영상 파일을 고르는 흐름이다. 영상은 별도로 공유하고, 앱은 리더 조작에 맞춰 재생하도록 동기화 신호를 전달하는 구조로 설계했다.
서로 같은 영상을 골랐는지 확인하기 위해 파일 일부로 계산한 식별값인 ‘영상 지문’을 대조한다. 이를 계산하는 공통 스크립트가 fingerprint.js였고, 페이지는 이 스크립트가 준비하는 window.FormationFingerprint 객체의 함수를 사용했다. 영상 선택 뒤 버튼이 켜지지 않는 문제를 쫓다가, 그보다 앞선 공통 스크립트 초기화 실패를 발견한 사건이다.
증상
안드로이드에서 영상 파일명은 보였지만 방 만들기와 참여 버튼이 활성화되지 않았다. 뷰어는 방 정보를 불러오는 상태에 머물렀고, 아이폰과 데스크톱은 됐다. 이후 같은 안드로이드 폰에서도 일반 크롬은 되고 웨일은 안 된다는 비교가 나왔다.
8월 25일의 첫 제보 이후 8월 30일에도 진행할 수 없다는 보고가 이어졌다. 화면 디버그 패널을 붙인 뒤 받은 로그의 핵심은 다음과 같다. 환경은 Android 10의 Whale 3.9.14.9(Chrome 138 기반)였다. 오류 뒤에 붙어 있던 배포 주소는 생략했다.
secureContext: true | crypto.subtle: true | Blob.arrayBuffer: true | File.text: true
❌ window.error: Uncaught TypeError: Cannot destructure property 'fingerprintVideo' of 'window.FormationFingerprint' as it is undefined.
원인
로그가 확인해 주는 것은 나열한 API의 지원 여부와 구조분해 시점의 객체 부재다. 지원 표시만으로 암호화나 파일 읽기 호출까지 정상이라고 판정할 수는 없다. 또한 객체가 없다는 사실만으로 다운로드 차단과 실행 실패를 구별할 수도 없다.
당시에는 .html 없는 /leader에서 상대경로가 잘못됐을 가능성도 의심했다. 하지만 데스크톱에서 같은 경로로 열었을 때 객체가 정상이라는 확인이 나와, 그 경로만으로 웨일의 실패를 설명하기는 어려워졌다. 이후 파일명 차단 가설에 따라 이름을 바꿨고 정상 동작 확인을 받았다. 차단 기능 자체를 특정한 실험 결과는 남아 있지 않다.
해결
fingerprint.js를 mediacheck.js로 바꾸고 두 페이지의 참조를 수정해 배포했다. 파일 헤더 주석에도 이름을 바꾼 이유를 기록했다. 데스크톱에서는 새 파일 로드와 기존 FormationFingerprint 객체, 함수 존재를 확인했다.
이어 “된다!! 됐어! 잘했다!”라는 응답과 파일 선택 필터를 다시 넣어 달라는 요청이 왔다. 마지막에는 확장자 필터 복원과 배포가 보고됐지만, 복원한 필터가 웨일에서 원하는 화면을 띄우는지에 대한 후속 확인은 없다. 파일명 변경의 성공과 필터 복원의 검증 상태는 별개다.
재발 방지 설계에서는 필수 객체가 없을 때 명확한 오류를 표시하고, 그 객체에 의존하는 버튼과 처리를 중단하는 편이 낫다. 단순히 함수를 null로 두고 초기화를 계속하는 예제는 이후 호출부까지 보호하지 못하므로 여기서는 제시하지 않는다. 이는 이번에 적용한 내역이 아니라 추가 설계 제안이다.
취재 후기 — 파일을 고르기 전에 이미 멈춰 있었다
기록에 남은 가설과 관찰을 네 사람이 직접 디버깅하는 장면으로 재구성했다.
readDuration이 기다리는 건 아닐까? loadedmetadata, 즉 영상의 기본 정보가 준비됐다는 신호가 안 오면 다음 단계로 못 가잖아. accept 값 때문일까? 모든 영상 형식을 뜻하는 video/*가 사진·영상 선택 화면을 부르는 게 아닌지 의심돼. ?debug=1로 열면 파일 선택과 길이 읽기, 영상 구별용 식별값인 지문 계산 과정을 볼 수 있어. window.FormationFingerprint라는 공용 객체가 없는 상태인 undefined라서, 안에 있어야 할 함수를 꺼내지 못한대. fingerprintVideo가 있어야 해. 객체에서 값을 꺼내는 구조분해가 먼저 실패해서, 뒤의 파일 처리와 통신도 시작 못 했다는 설명이 맞아떨어져. .html 없는 /leader여서 스크립트의 상대경로가 달라진 건 아닐까? 현재 페이지를 기준으로 파일 위치를 찾다가 다른 곳을 보는지 확인해야겠어. /leader로 열어 객체가 있는지 확인하자. 같은 경로에서도 정상이라면 주소 모양만으로 웨일의 실패를 설명하기 어려워져. /leader에서도 객체가 정상으로 잡혀. 내 웨일에서는 페이지에 들어오자마자 객체가 없다는 오류가 나니까 차이가 남아 있어. fingerprint.js를 mediacheck.js로 바꾸고 두 페이지의 참조도 바꿔 보자.