개발 취재록

모바일 브라우저(웨일)에서만 안 되는데 콘솔을 볼 수가 없을 때 — ?debug=1 화면 디버그 패널

무용 연습실에서 각자 준비한 영상을 리더의 조작에 맞춰 함께 보는 영상 동기화 웹앱을 만들고 있었다. 매번 노트북 서버를 켜는 번거로움을 줄이려고 클라우드로 옮겼는데, 안드로이드 웨일에서 영상을 골라도 방 만들기·참여 버튼이 켜지지 않았다.

결론

모바일 브라우저에서만 나는 버그를 추측으로 고치고 있다면, 멈추고 ?debug=1일 때만 화면에 뜨는 로그 패널부터 넣는다. 핵심은 네 가지다.

  1. 맨 먼저 로드되는 스크립트로 넣는다. 그래야 페이지 로드 시점에 터진 에러까지 잡힌다
  2. 첫 줄에 UA와 쓰는 API 지원 여부를 찍는다
  3. window.onerror(와 unhandledrejection)를 화면으로 보낸다
  4. 복사 버튼을 단다. 사용자가 로그를 그대로 붙여 넣을 수 있게

이 사건에서는 첫날 가설 네 개를 추측으로 쫓으며 코드를 여러 번 고쳤는데도 풀리지 않았고, 5일 뒤에도 증상은 그대로였다 . 패널을 배포하자 사용자가 첫 로그를 보냈고, 그 로그 한 줄로 원인이 바로 나왔다. 패널 커밋부터 수정 커밋까지 약 6분 걸렸다 .

아래는 같은 구성을 재현한 예시 구현이다(원본 debug.js 코드가 아니다. 원본의 구성 요소는 에 나열돼 있다).

<!-- 페이지의 첫 번째 <script>. 다른 어떤 스크립트보다 먼저 -->
<script src="/debug.js"></script>
// debug.js — ?debug=1 일 때만 켜지는 화면 로그 패널 (예시)
(function () {
  const on = new URLSearchParams(location.search).get('debug') === '1';
  const lines = [];
  let box;

  function t() { return new Date().toISOString().slice(11, 23); }
  function render() { if (box) box.textContent = lines.join('\n'); }

  window.dbg = function (...args) {
    if (!on) return;
    const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
    lines.push(t() + '  ' + msg);
    render();
  };
  if (!on) return;

  // 1) 페이지 로드 중 에러도 잡도록 가장 먼저 등록
  window.addEventListener('error', e =>
    dbg('❌ window.error:', e.message, '@', (e.filename || '') + ':' + e.lineno + ':' + e.colno));
  window.addEventListener('unhandledrejection', e =>
    dbg('❌ unhandledrejection:', String(e.reason)));

  // 2) 환경 한 줄: 가설을 한 번에 지우는 줄
  dbg('=== debug on ===');
  dbg('UA:', navigator.userAgent);
  dbg('secureContext:', window.isSecureContext,
      '| crypto.subtle:', !!(window.crypto && crypto.subtle),
      '| Blob.arrayBuffer:', typeof Blob !== 'undefined' && 'arrayBuffer' in Blob.prototype,
      '| File.text:', typeof File !== 'undefined' && 'text' in File.prototype);

  // 3) 패널 + 복사 버튼 (body가 생긴 뒤 붙이기)
  document.addEventListener('DOMContentLoaded', () => {
    const wrap = document.createElement('div');
    wrap.style.cssText = 'position:fixed;left:0;right:0;bottom:0;max-height:40vh;overflow:auto;' +
      'background:#000c;color:#0f0;font:11px/1.4 monospace;z-index:99999;padding:6px';
    const btn = document.createElement('button');
    btn.textContent = '복사';
    btn.onclick = () => navigator.clipboard.writeText(lines.join('\n'))
      .then(() => btn.textContent = '복사됨', () => btn.textContent = '복사 실패');
    box = document.createElement('pre');
    box.style.cssText = 'margin:0;white-space:pre-wrap';
    wrap.append(btn, box);
    document.body.append(wrap);
    render();
  });
})();

그다음 의심 가는 파이프라인에 dbg()를 한 줄씩 끼운다. 이 사건에서는 파일 정보 → 길이 읽기 시작/완료 → 지문 계산 시작/완료 → 버튼 disabled 최종값 순서였다 .

chrome://inspect 원격 디버깅화면 디버그 패널 (?debug=1)
준비물PC, USB 케이블, 폰의 개발자 옵션 → USB 디버깅 없음. URL에 ?debug=1만 붙이면 됨
보이는 것콘솔·네트워크 등 진짜 DevTools 전부직접 심은 로그 + 전역 에러만
버그가 난 사람의 폰에서그 폰을 PC에 꽂아야 함링크만 보내면 됨. 로그는 복사해서 받음
이 사건에서시도하지 않음첫 로그로 원인 확정

네트워크 요청까지 봐야 하면 chrome://inspect가 낫다. "어디서 죽었는지"만 알면 되는 경우라면 패널이 훨씬 빠르다.

상황

리더가 연습방을 열고 영상을 선택하면, 참가자는 방 목록에서 같은 방에 들어가 각자 미리 받은 영상 파일을 고른다. 서버는 영상 대신 동기화 신호를 전달하고, 참가자의 재생은 리더 조작을 따른다.

같은 영상인지 확인하는 지문 계산 헬퍼는 별도 스크립트였다. 파일 선택 뒤 길이 읽기와 지문 계산을 거쳐 버튼 상태를 정하고, 뷰어는 서버에서 방 정보를 받아 영상 대조에 쓴다. 따라서 버튼이 꺼져 있다는 관찰만으로 어느 단계가 고장 났는지는 알 수 없었다.

증상

이 사건에서는 문제 기기의 콘솔 로그 없이 수정을 반복했고, 이후 모바일 디버깅 방법을 찾으면서 화면 패널을 넣었다.

원인

두 층위로 봐야 한다.

버그 자체의 원인. 패널이 찍은 첫 로그는 이랬다(배포 주소는 제거했다) .

UA: Mozilla/5.0 (Linux; Android 10; K) ... Chrome/138.0.0.0 Whale/3.9.14.9 Mobile Safari/537.36
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. @ /leader?debug=1:172:13

파일을 고르기도 전, 페이지를 열자마자 에러가 났다. 별도 파일로 둔 영상 지문 헬퍼 fingerprint.js가 웨일에서만 로드되지 않았다. 그래서 인라인 스크립트가 맨 위 구조분해에서 죽었고, 그 아래의 파일 핸들러·WebSocket 연결·버튼 활성화 코드가 전부 등록되지 않았다 . 버튼이 안 켜진 것도, "방 정보를 불러오는 중"에서 멈춘 것도 이 에러 하나로 설명된다 .

파일 이름을 mediacheck.js로 바꾸자 웨일에서도 됐다 . 바꾼 것은 파일명과 참조뿐이고, 전역 이름 FormationFingerprint와 함수 이름은 그대로 뒀다 . 그래서 막힌 기준은 파일명(URL)이었다고 보는 게 자연스럽다. 다만 웨일의 어떤 기능이나 필터 목록이 막았는지는 확인하지 않았다. "추적 방지 기능이 fingerprint를 지문 추적 스크립트로 오인했다"는 설명은 에이전트의 추론이다 .

첫날 원인을 못 잡은 이유. 에러를 볼 수단이 없어서 증상만 보고 고쳤다. 5일은 계속 매달린 시간이 아니라 첫날(8월 25일) 시도와 다음 시도(8월 30일) 사이의 간격이다. 확인된 사실은 이 둘뿐이다.

해결

1. 두 길 중 패널을 고른 이유

에이전트는 chrome://inspect 원격 디버깅과 앱 안 디버그 패널을 함께 제시했다. 원격 디버깅은 케이블과 설정이 필요했다. 패널은 웨일에서 설정 없이 바로 된다는 이유로 패널을 먼저 택했다 . 에이전트는 웨일도 원격 디버깅이 "대개 된다"고 했지만, 이 사건에서는 시도하지 않았으므로 여기서는 확인하지 않은 주장으로 둔다 .

2. 패널 구성

사용법 안내에 적힌 구성은 이렇다 .

debug.js는 두 페이지 모두에 첫 번째 스크립트로 넣었다 . 이 사건에서 가장 중요했던 결정이 이것이다. 실제 에러는 파일 선택 파이프라인이 아니라 페이지 로드 시점에 났다. 패널이 앱 스크립트보다 먼저 떠 있고 window.error를 듣고 있었기 때문에 잡을 수 있었다 .

3. 첫 줄이 한 일

패널 설계 단계에서 에이전트는 로그를 읽는 기준을 미리 정해 뒀다. crypto.subtle: false면 웨일이 보안 API를 막는 것, size: 0이면 파일 바이트가 넘어오지 않는 것, 이런 식이다 . 실제 첫 줄은 네 항목이 모두 true였다 . 이 값은 해당 환경·API의 존재 여부를 보여줄 뿐, 실제 호출 성공까지 보장하지는 않는다. 이 사건에서 실행 중단 지점을 알려준 결정적 증거는 바로 아래의 window.error 한 줄이었다.

4. 로그를 받은 뒤

덤 — 같은 사건의 곁가지 교훈

취재 후기 — 버튼을 고치기 전에 첫 에러부터

박도은
연습실에서 각자 폰으로 같은 영상을 맞춰 보는 웹앱을 만들고 있잖아. 내 안드로이드에선 파일명은 뜨는데 방 만들기와 참여 버튼이 안 켜져.
정하늘
영상을 골랐다는 표시만으로 준비가 끝났다고 볼 수는 없어. 길이 읽기, 같은 영상인지 확인하는 지문 계산, 버튼 활성화 중 어디서 멈췄는지 나눠 보자.
정바다
loadedmetadata, 영상 길이 같은 기본 정보가 준비됐다는 신호가 안 오는 것 아닐까? 길이 읽기를 계속 기다리면 버튼도 켜지지 않을 거야.
정하늘
그 가설대로라면 기다림을 끊었을 때 진행돼야겠네. 길이를 못 읽어도 6초 뒤 넘어가게 하고, 서버도 길이를 모르는 파일을 받도록 바꿔 확인하자.
박도은
바꾼 뒤 다시 해봤는데 여전히 참여 버튼이 꺼져 있어. 아이폰과 데스크톱은 되고, 이 폰은 ‘방 정보를 불러오는 중’에서 멈춰.
정하늘
그러면 길이 읽기 대기를 풀었다고 해결됐다는 판정은 취소야. 방 정보를 못 받는다는 관찰도 생겼으니, 파일 처리만 볼 수는 없겠어.
박도현
기다림을 제한하는 코드는 대비책이 될 수 있어도 원인의 증거는 아니야. 문제 기기에서 그대로라면 ‘고쳤다’는 말부터 거둬야 해.
정바다
방 정보를 못 받는다면 WebSocket, 서버와 연결을 유지하며 메시지를 주고받는 통로가 문제 아닐까? 영상 대조에 필요한 방 정보도 그쪽으로 오잖아.
정하늘
배포된 뷰어를 데스크톱에서 열어 연결과 방 정보 수신을 확인하자. 폰 화면에도 연결 상태를 표시해서, 어느 환경에서 끊기는지 비교해 보자.
박도은
데스크톱 뷰어는 연결되고 방 정보도 받아. 폰에서도 브라우저를 비교해 보니 일반 크롬은 되고, 웨일에서만 안 되는 거였어.
정하늘
서버 전체의 연결 장애로 설명하기는 어려워졌어. 다만 데스크톱 성공만으로 웨일의 연결까지 정상이라고 판정할 수는 없어.
박도현
‘방 정보 대기 중’은 결과 화면이지 통신 장애를 직접 찍은 로그가 아니야. 연결을 시작하는 코드가 실행됐는지부터 확인할 필요가 있어.
정바다
크롬은 되는데 웨일만 안 되니까, 웨일의 차단 기능이 통신을 막는 것 아닐까? 브라우저에 따라 갈리는 증상은 그 가설과 맞아 보여.
정하늘
그러면 웨일의 연결 상태와 차단 설정을 바꾼 결과가 있어야 해. 연결 실패가 실제로 찍히는지 확인하기 전에는 차단이라고 확정하지 말자.
박도은
지금 눈에 보이는 다른 단서도 있어. 파일을 열면 사진 앱처럼 보이는 창이 뜨고, 기본 설정을 지워도 클라우드 미디어 안내가 나와.
정하늘
그건 파일 선택 화면의 관찰이야. 통신 차단을 확인한 결과는 아직 없으니 그 가설은 미확인으로 남겨 두고, 선택 과정은 따로 보자.
박도현
브라우저가 다르다는 단서로 어느 기능이 막혔는지까지 알 수는 없어. 통신 차단으로 단정하면 실제로 멈춘 코드를 놓치게 돼.
정바다
사진 선택 도구, 그러니까 사진과 영상을 고르는 전용 창으로 들어가서 문제인 것 아닐까? 클라우드 미디어 안내가 뜬다는 게 근거야.
정하늘
accept는 파일 입력에서 고를 형식을 지정하는 속성이야. 영상 형식 지정과 확장자 필터를 차례로 바꾸고, 그래도 안 되면 속성을 빼서 비교하자.
박도은
동선용 JSON 파일은 고를 수 있는데 영상은 수정 뒤에도 안 돼. 필터를 아예 뺀 뒤에도, 5일 후 다시 해보니 재생 가능한 상태로 넘어가지 않아.
정하늘
선택 조건 변경으로 이 증상이 해결되지는 않았어. 이제는 화면에 로그를 띄워서 파일 선택 전후 중 어느 시점에 에러가 나는지 확인하자.
박도현
선택 창이 수상해 보여도 버튼이 멈춘 원인과 같다는 보장은 없어. 바꿔도 그대로라면 다음 추측보다 실행 기록을 얻는 데 시간을 써야 해.
정하늘
케이블로 PC에 연결하는 원격 디버깅도 있지만, 이번엔 ?debug=1 화면 패널을 먼저 넣자. 앱보다 먼저 읽히게 해서 시작할 때 나는 에러도 잡자.
정바다
UA라는 브라우저 식별 정보와 API, 즉 코드가 쓰는 기능의 지원 여부도 찍자. 파일 처리 단계별 로그와 복사 버튼을 함께 두면 어디서 멈췄는지 읽을 수 있겠어.
박도은
웨일에서 페이지를 열자마자 에러가 보여. 파일은 아직 안 골랐어. window.FormationFingerprint가 없어서 fingerprintVideo를 꺼낼 수 없다고 나와.
정바다
영상 지문 함수를 담는 객체가 아예 없네. 주소가 .html 없는 /leader라서 별도 파일인 fingerprint.js를 찾는 상대경로가 어긋난 것 아닐까?
정하늘
그 경로 그대로 데스크톱에서도 열어 보자. 파일이 준비됐는지와 그 객체가 있는지 확인하면, 경로만으로 같은 문제가 나는지 비교할 수 있어.
박도은
데스크톱은 /leader로 열어도 객체가 있어. 웨일에서 본 건 파일 선택 뒤의 실패가 아니라 페이지를 열자마자 그 객체가 없다는 에러야.
정하늘
경로만의 문제라는 가설은 접자. 앞부분에서 실행이 멈춰 파일 선택 처리와 서버 연결도 시작하지 못한 거야. 두 증상이 같은 시작 오류로 설명돼.
박도현
파일을 고른 다음 멈춘 것처럼 보여도 실제 실패는 그보다 앞이었네. 진단 코드를 앱보다 먼저 읽혀야 한다는 이유가 바로 이거야.
정하늘
파일명을 mediacheck.js로 바꾸고 두 페이지의 참조도 바꿨어. 함수와 전역 객체 이름은 유지했고 데스크톱에서도 확인했어. 이제 웨일 결과를 보자.
박도은
이제 돼! 이름을 바꾼 뒤에는 진행할 수 있어. 다만 지금 확인한 성공만으로 웨일의 어느 차단 설정이 원인이었는지까지 알 수는 없어.
박도현
첫 에러가 있어야 고장 난 단계를 좁힐 수 있어. 콘솔을 바로 볼 수 없는 기기에서는 추측을 더하기 전에, 앱보다 먼저 실행되는 로그 패널부터 준비하자.