개발 취재 노트

안드로이드에서 동영상 업로드가 구글 포토로만 열릴 때 — accept는 필터가 아니라 선택기를 바꾼다

무용 연습실에서 각자 폰에 둔 안무 영상을 리더의 조작에 맞춰 함께 보는 웹앱(formation)을 만들고 있었다. 안드로이드 폰에서 영상 파일을 선택하면 파일명은 보이는데 방 만들기·참여 버튼이 켜지지 않았고, 선택 화면을 열면 구글 포토가 뜨거나 "클라우드 미디어에 액세스합니다"라는 안내만 나왔다 .

결론

<input type="file" accept="video/*">처럼 accept에 이미지·동영상 MIME만 있으면, 안드로이드(크로미움 계열 브라우저)는 일반 파일 선택기 대신 시스템 "사진 선택 도구(Photo Picker)"를 띄울 수 있다. 이 선택기는 사진첩·클라우드 미디어 중심으로 동작해서, 카카오톡이나 다운로드 폴더에 받아둔 영상은 목록에서 찾기 어려울 수 있다. 이 동작은 세션 밖 Chromium 소스 코드의 파일 선택 로직으로도 확인된다.

흔히 쓰는 회피법인 "accept를 확장자만(.mp4,.mov,.webm 등) 나열하면 문서/파일 선택기로 바뀐다"는 믿을 수 없다. Chromium 내부에서는 확장자를 안드로이드 MimeTypeMap으로 다시 MIME 타입으로 변환한 뒤 같은 조건을 검사하므로, .mp4·.mov·.webm처럼 흔한 확장자는 결국 video/mp4·video/quicktime으로 되돌아가 여전히 사진 선택 도구가 뜰 수 있다. 확인된 회피법은 두 가지뿐이다.

<!-- 문제: 미디어 MIME 와일드카드만 있으면 사진 선택 도구가 뜬다 -->
<input type="file" accept="video/*">

<!-- 신뢰할 수 없음: 흔한 확장자는 내부에서 다시 video/* MIME으로 변환돼
     여전히 사진 선택 도구가 뜰 수 있다 -->
<input type="file" accept=".mp4,.mov,.webm">

<!-- 확인된 회피법 1: accept를 아예 없앤다 -->
<input type="file">

<!-- 확인된 회피법 2: image/ 또는 video/로 시작하지 않는 MIME을 하나 끼워 넣는다
     ("모든 항목이 image/video로 시작해야 한다"는 조건 자체가 깨진다) -->
<input type="file" accept="video/mp4,application/x-not-a-real-type">

하지만 "파일명은 보이는데 버튼이 안 켜진다"면 accept부터 만지지 말고 먼저 이것부터 확인한다. 이번 사고에서 버튼을 실제로 막은 직접 원인은 accept나 사진 선택 도구가 아니라, 별도 스크립트 파일이 브라우저에 로드되지 않아 그 스크립트가 정의해야 할 전역 객체가 undefined인 채로 인라인 스크립트가 페이지 진입 직후 그 줄에서 예외를 던지고 통째로 멈춘 것이었다 — 파일 선택 이후 코드(버튼 활성화 포함)가 아예 실행될 기회조차 없었다 . 원인은 안드로이드 웨일 브라우저의 추적 방지 기능이 그 파일명(fingerprint.js)을 "브라우저 지문 추적 스크립트"로 오인해 차단한 것이었다 . 그래서 첫 확인 순서는 이렇다: 주소 끝에 디버그 플래그를 붙이거나(이 사례는 ?debug=1) 브라우저 콘솔을 켜고, 파일을 고르기도 전에 이미 "Cannot destructure property ... of ... undefined" 같은 초기화 오류가 찍혀 있는지부터 본다. 그런 오류가 있다면 accept를 아무리 바꿔도 소용없다 — 공용 스크립트 파일명이 광고·추적 차단 목록의 키워드(fingerprint, tracker, analytics 등)와 겹치지 않는지부터 보고, 겹친다면 이름을 바꿔 다시 배포한 뒤에야 위 accept 회피법을 시도한다. 오류가 없다면 그때 위 회피법 1·2를 시도하면 된다.

상황

이 앱은 리더가 방을 만들고 영상 파일을 고르면, 참여자들은 방 목록에서 자기 방에 들어가 각자 준비한 영상을 골라 리더의 재생 위치에 맞춰 보는 구조다. 처음엔 노트북 로컬 서버였고, 이후 공개 주소로 접속해 WebSocket으로 재생 신호를 주고받는 클라우드 버전으로 옮겼다 . 참여자 기기가 안드로이드·아이폰, 크롬·웨일 등으로 제각각이라 기기·브라우저별 문제가 자주 생겼다.

증상

배포 직후 데스크톱·아이폰에서는 방 생성과 참여가 정상이었지만, 안드로이드에서는 영상을 선택해도 파일명만 보일 뿐 방 만들기·참여 버튼이 활성화되지 않았다. 뷰어 화면은 "방 정보를 불러오는 중"에 계속 머물렀다 .

파일 선택 화면을 열면 기본 설정된 구글 포토 앱이 바로 뜨는 것 같다는 관찰이 나왔다. 기본 설정을 지운 뒤에도 "클라우드 미디어 설정", "앱 또는 웹사이트에서 사진이나 동영상을 선택하라고 요청하면 클라우드 미디어에 액세스합니다"라는 안내가 떴다 — 안드로이드 13부터 들어간 시스템 사진 선택 도구(Photo Picker)의 전형적인 화면이다 .

원인

accept="video/*"가 안드로이드에서 "영상 고르기" 인텐트를 띄우고, 사진 선택 도구가 기본으로 열리는 것은 실제로 확인된 동작이다 . Chromium 소스의 파일 선택 로직(SelectFileDialog.java)은 변환된 MIME 목록이 전부 image/나 video/로 시작할 때 사진 선택 도구를 사용할 수 있다고 판정한다. 사진 선택 도구는 카메라 롤·클라우드 미디어 라이브러리 중심으로 동작해서, 카톡으로 받아 다운로드 폴더에 저장한 mp4를 찾기 어렵게 만든다.

문제는 그다음 시도였다. video/*를 빼고 구체적 MIME으로 바꿔도 안 됐고 , MIME을 모두 빼고 확장자만(.mp4,.mov,.webm,...) 남겨도 "안돼네"라는 보고가 나왔다 . 이 세션의 실패 보고만으로는 "확장자 방식 자체가 틀렸다"고 단정할 수 없다 — 이후 밝혀졌듯 이 시점엔 별도로 fingerprint.js라는 공용 스크립트가 웨일에 의해 로드되지 않아 인라인 스크립트 전체가 초기화 단계에서 죽어 있었고, 그 상태에서는 어떤 accept 값을 넣어도 버튼이 켜질 수 없었다 . 즉 세션 안에서 관찰된 "확장자 방식 실패"는 accept 메커니즘과 무관한 다른 장애와 뒤섞인 결과다.

다만 세션 밖에서 Chromium 소스를 확인한 결과, 확장자 방식이 애초에 신뢰할 수 없는 방법이라는 독립적인 근거가 나온다. convertToSupportedMimeTypes()는 accept에 들어온 확장자를 안드로이드 MimeTypeMap.getMimeTypeFromExtension()으로 MIME 타입으로 바꾼 뒤 같은 "전부 image/video인가" 검사를 통과시킨다. .mp4 → video/mp4, .mov → video/quicktime처럼 표준으로 인식되는 확장자는 변환을 거쳐도 여전히 조건을 만족해 사진 선택 도구가 뜰 수 있다. 반대로 image/나 video/로 시작하지 않는 MIME을 하나 섞으면, "모든 항목이 사진 선택 도구 지원 MIME인가"라는 조건이 깨져 일반 파일 선택기로 빠질 수 있다.

해결

이 세션 자체에서 accept 변경만으로 문제가 풀렸다는 검증은 없다. 웨일에서 실제로 해결된 것은 fingerprint.js를 mediacheck.js로 바꿔 죽어 있던 초기화 스크립트를 되살린 것이었다 . 그 뒤 사용자 요청으로 영상 입력에 accept=".mp4,.m4v,.mov,.webm,.mkv,.avi,.3gp,.ts,.mpeg,.mpg"가 다시 적용돼 배포됐지만, 이 값이 실제로 원하는 선택 화면(사진 선택 도구가 아닌 파일 선택기)을 띄웠는지는 세션에서 확인되지 않았다 .

같은 문제를 만난 독자에게 실용적인 결론은 이렇다. 카톡·다운로드 폴더의 영상을 안드로이드 웹에서 고르게 하고 싶다면, 확장자만 나열하는 방식에 기대지 말고 위 코드의 회피법 1(accept 완전 제거) 또는 회피법 2(image/·video로 시작하지 않는 MIME 추가)를 먼저 시도하는 편이 안전하다. 파일 형식 제한이 꼭 필요하면 accept로 거르는 대신, 선택된 파일을 JS에서 확장자·MIME으로 다시 검사하는 편이 사진 선택 도구 문제와 분리된다.

덤 — 증상이 같아도 원인은 하나가 아닐 수 있다

이 사건에서는 "파일 선택기가 이상하다"는 관찰과 "버튼이 안 켜진다"는 증상이 사실 서로 다른 두 장애였다. accept 메커니즘(사진 선택 도구)은 실제로 존재하는 문제였지만, 이 세션에서 버튼을 막은 직접 원인은 웨일이 fingerprint.js라는 파일명을 추적 스크립트로 오인해 차단한 것이었다. accept를 아무리 고쳐도 공용 스크립트가 죽어 있는 동안은 어떤 값도 버튼을 켜지 못했다. 파일 선택 전에 이미 스크립트 초기화가 실패하고 있었는지부터 화면 로그로 확인하는 편이 먼저다.

취재 후기 — 필터를 고쳤는데 왜 매번 "안돼"였을까

박도은
연습실에서 각자 폰으로 같은 안무 영상 보는 앱 있잖아. 안드로이드에서 영상 고르면 방 만들기 버튼이 안 켜져. 구글 포토가 뜨는 것도 이상하고.
정바다
그거 딱 짐작 가는데. accept="video/*"를 쓰면 안드로이드가 사진첩 전용 선택 창을 띄우거든. 구글 포토가 기본이면 바로 그리로 튀어.
정하늘
그러면 카톡으로 받은 파일은 왜 안 보이는 건데? 사진첩엔 카메라로 찍은 것만 있잖아.
정바다
그게 "사진 선택 도구(Photo Picker)"라는 안드로이드 시스템 기능이야. 사진첩이랑 클라우드 미디어만 보여주고, 다운로드 폴더는 못 뒤져.
박도은
그럼 video/*를 빼고 확장자만 쓰면 되는 거 아냐? .json은 파일 선택창이 멀쩡히 뜨거든.
정하늘
해볼 만한 가설이네. 확장자만 남기고 배포했어. 이번엔 뭐라고 떠?
박도은
여전히 안돼. 파일명 보여주고 끝이야, 버튼이 안 켜져.
정바다
이상하다, 확장자만 쓰면 문서 선택기로 빠져야 하는데… 그럼 accept를 아예 없애 버리자.
박도현
잠깐, 그 전에. 확장자로 바꿔도 안 됐다는 게 "선택 화면이 그대로"라는 거야, 아니면 "선택은 됐는데 버튼이 안 켜진다"는 거야? 둘은 다른 문제야.
박도은
뒤엣것. 화면 디버그 패널을 붙였더니, 파일을 고르기도 전에 이미 오류가 찍혀 있어. window.FormationFingerprint가 undefined래.
정하늘
그게 뭔데? 파일 선택이랑 무슨 상관이야?
박도현
영상 지문(같은 파일인지 대조하는 값)을 계산하는 공용 객체야. 그게 없으면 페이지 진입 직후 스크립트 전체가 그 줄에서 멈춰. 파일 선택 이후 코드는 아예 실행될 기회가 없었던 거지.
정바다
그럼… 내가 확장자 필터 바꾼 게 애초에 시험도 못 해본 거네? 스크립트가 죽어 있으면 어떤 accept를 넣어도 버튼이 안 켜지잖아.
정하늘
맞아. 일단 그 죽은 스크립트부터 살리자. 파일명이 fingerprint.js인데, 웨일이 이 이름을 추적 스크립트로 오인해서 막는 것 같아. mediacheck.js로 바꿔서 배포할게.
박도은
됐다!! 버튼 켜져!
박도현
좋아, 근데 그거랑 별개로 필터 문제는 아직 안 끝났어. 확장자만 쓰면 안전하다고 확신할 수 있어?
정하늘
아니, 사실 그건 이 세션만으로는 확인이 안 돼. 죽어 있던 스크립트 때문에 필터 효과 자체를 못 봤으니까. 확장자 필터는 다시 넣어서 배포는 했는데, 실제로 원하는 선택 화면이 뜨는지는 미확인이야.
박도현
그럼 "확장자면 된다"고 말하고 끝내면 안 되겠네. 증상이 하나처럼 보여도 원인이 둘이면, 하나를 고쳤다고 나머지가 검증된 건 아니야.