개발 취재록

크롬은 되는데 웨일 브라우저에서만 스크립트가 안 돌 때 — Cannot destructure property of undefined

각자 폰에 준비한 안무 영상을 리더의 조작에 맞춰 재생하는 무용 연습용 영상 동기화 웹앱을 만들고 있었다. 노트북에 서버를 매번 띄우는 번거로움을 줄이려고 동기화 신호를 클라우드로 옮긴 뒤, 안드로이드 웨일에서 영상을 골라도 방 만들기와 참여 버튼이 켜지지 않았다. 같은 폰의 일반 크롬에서는 동작했다.

결론

이번 웨일(안드로이드) 장애는 fingerprint.jsmediacheck.js로 바꾸고 두 페이지의 참조를 수정한 뒤 해결됐다. 배포 후 정상 동작했다는 응답이 있었다.

직접 확인된 오류는 페이지 진입 직후 window.FormationFingerprintundefined여서 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.jsmediacheck.js로 바꾸고 두 페이지의 참조를 수정해 배포했다. 파일 헤더 주석에도 이름을 바꾼 이유를 기록했다. 데스크톱에서는 새 파일 로드와 기존 FormationFingerprint 객체, 함수 존재를 확인했다.

이어 “된다!! 됐어! 잘했다!”라는 응답과 파일 선택 필터를 다시 넣어 달라는 요청이 왔다. 마지막에는 확장자 필터 복원과 배포가 보고됐지만, 복원한 필터가 웨일에서 원하는 화면을 띄우는지에 대한 후속 확인은 없다. 파일명 변경의 성공과 필터 복원의 검증 상태는 별개다.

재발 방지 설계에서는 필수 객체가 없을 때 명확한 오류를 표시하고, 그 객체에 의존하는 버튼과 처리를 중단하는 편이 낫다. 단순히 함수를 null로 두고 초기화를 계속하는 예제는 이후 호출부까지 보호하지 못하므로 여기서는 제시하지 않는다. 이는 이번에 적용한 내역이 아니라 추가 설계 제안이다.

취재 후기 — 파일을 고르기 전에 이미 멈춰 있었다

기록에 남은 가설과 관찰을 네 사람이 직접 디버깅하는 장면으로 재구성했다.

박도은
각자 폰에 둔 안무 영상을 리더 조작에 맞춰 보는 웹앱을 만들고 있잖아. 내 안드로이드에서는 파일명까지 보이는데 방 만들기와 참여 버튼이 안 켜져.
정바다
영상 길이를 읽는 readDuration이 기다리는 건 아닐까? loadedmetadata, 즉 영상의 기본 정보가 준비됐다는 신호가 안 오면 다음 단계로 못 가잖아.
정하늘
그럼 기다리는 시간을 제한하고, 길이를 못 읽어도 진행하게 바꿔 보자. 수정한 버전을 문제 기기에서 열어 버튼이 켜지는지 확인하면 돼.
박도은
다시 해도 참여 버튼이 안 켜져. 아이폰과 데스크톱은 되는데, 내 안드로이드에서는 방 정보를 불러오는 중이라는 문구가 계속 남아 있어.
정하늘
그러면 길이 읽기 수정으로 이 장애를 해결했다고 판정할 수 없어. 기다림을 제한했는데도 문제 기기의 증상이 그대로 남았으니 다음 근거가 필요해.
박도현
멈출 수 있는 코드를 보강한 것과 실제 멈춘 원인을 찾은 건 구별해야 해. 다른 환경의 성공만으로 문제 기기까지 고쳤다고 결론 내리면 안 돼.
정바다
방 정보가 안 오니 WebSocket 문제일 수도 있겠어. 서버와 연결을 유지하며 정보를 주고받는 통신인데, 이 연결이 안 되면 방 정보를 못 받잖아.
정하늘
먼저 배포된 뷰어, 그러니까 참여자 화면에서 방 정보를 받는지 확인하자. 연결 상태도 화면에 표시하고, 폰에서는 브라우저를 바꿔 비교해 보자.
박도은
데스크톱 뷰어는 연결됐고 방 정보도 받았어. 폰에서도 다시 비교했는데 일반 크롬은 잘 돼. 지금 실패하는 건 내가 쓰는 웨일이야.
정하늘
조사 범위는 웨일로 좁혀졌어. 다만 다른 브라우저에서 연결됐다는 결과만으로 웨일의 통신이 어디에서 멈췄는지까지 판정할 수는 없어.
박도현
방 정보를 못 받았다는 건 결과야. 연결을 시도했다가 실패한 건지, 연결을 시작하는 코드까지 가지 못한 건지 구분해야 원인을 좁힐 수 있어.
정바다
웨일에서만 실패한다면 광고나 추적을 막는 기능이 통신을 차단하는 건 아닐까? 브라우저별 차이가 있으니 그 설정도 의심해 볼 만해.
정하늘
연결 상태 표시를 보고 차단 설정을 바꿔 재시도하면 그 가설을 시험할 수 있어. 하지만 설정 변경 전후의 결과는 아직 없으니 통신 차단으로 확정하지 말자.
박도은
지금 새로 눈에 띄는 건 파일 선택 화면이야. 구글 포토가 열리는 것 같고, 기본 설정을 지워도 클라우드 미디어에 접근한다는 안내가 나와.
박도현
그 관찰은 파일 선택 화면에 관한 거라 통신 차단을 입증하지는 못해. 새로운 단서로 넘어가더라도 앞의 가설은 미확인으로 남겨 두자.
정바다
파일 형식을 제한하는 accept 값 때문일까? 모든 영상 형식을 뜻하는 video/*가 사진·영상 선택 화면을 부르는 게 아닌지 의심돼.
정하늘
그 값을 구체적인 형식으로 바꾸고, 다음에는 확장자만 남겨 비교하자. 선택 화면이 달라지는지와 선택 뒤 진행되는지를 나눠서 봐야 해.
박도은
형식을 바꿔도 마찬가지였고 확장자만 남겨도 안 됐어. 필터를 아예 뺀 뒤에도, 닷새 뒤 다시 해 보니 파일 선택 후 재생할 상태로 넘어가지 못해.
정하늘
필터 수정으로 전체 문제가 해결되지는 않았어. 다만 ‘안 된다’는 결과만으로 매번 선택 화면과 버튼 중 무엇이 그대로였는지까지 단정하지는 말자.
박도현
파일을 고르는 화면의 불편과 고른 뒤 처리가 멈추는 문제는 따로 확인해야 해. 눈에 보이는 화면을 여러 번 바꿨다고 원인에 가까워진 건 아니야.
정하늘
이제 오류를 화면에 보여주는 디버그 패널을 붙이자. ?debug=1로 열면 파일 선택과 길이 읽기, 영상 구별용 식별값인 지문 계산 과정을 볼 수 있어.
박도은
파일을 고르기도 전에 오류가 떠. window.FormationFingerprint라는 공용 객체가 없는 상태인 undefined라서, 안에 있어야 할 함수를 꺼내지 못한대.
정하늘
그 객체에는 영상 지문 계산 함수 fingerprintVideo가 있어야 해. 객체에서 값을 꺼내는 구조분해가 먼저 실패해서, 뒤의 파일 처리와 통신도 시작 못 했다는 설명이 맞아떨어져.
정바다
주소가 .html 없는 /leader여서 스크립트의 상대경로가 달라진 건 아닐까? 현재 페이지를 기준으로 파일 위치를 찾다가 다른 곳을 보는지 확인해야겠어.
정하늘
그럼 데스크톱에서도 정확히 /leader로 열어 객체가 있는지 확인하자. 같은 경로에서도 정상이라면 주소 모양만으로 웨일의 실패를 설명하기 어려워져.
박도은
데스크톱에서는 같은 /leader에서도 객체가 정상으로 잡혀. 내 웨일에서는 페이지에 들어오자마자 객체가 없다는 오류가 나니까 차이가 남아 있어.
정하늘
경로만으로는 이 차이가 설명되지 않아. 파일명 차단 가설을 시험하려면 fingerprint.jsmediacheck.js로 바꾸고 두 페이지의 참조도 바꿔 보자.
박도현
주소가 낯설게 보인다는 이유만으로 원인이라고 할 수는 없어. 같은 조건으로 비교해서 설명하지 못한 차이를 남겨야 다음 실험도 의미가 있어.
박도은
이름을 바꾼 버전으로는 이제 돼! 데스크톱에서도 새 파일이 로드되고 기존 객체와 함수가 확인됐어. 다음에는 파일 선택 필터를 다시 넣어야겠어.
정하늘
해결 확인은 이름을 바꾼 뒤 동작했다는 데까지야. 어떤 추적 방지 필터가 막았는지는 확인하지 못했고, 뒤에 복원한 파일 선택 필터도 실기기 결과를 더 확인해야 해.
박도현
여러 기능이 함께 멈추면 각각을 고치기 전에 최초 오류부터 확인하자. 효과가 확인된 해결 조치와 아직 검증하지 못한 원인 설명도 따로 남겨야 해.