본문으로 건너뛰기

앱 개발자에게 무엇을 요청할 것인가 - 웹뷰는 브라우저가 아니다 ep.09

여덟 편은 증상에서 출발했다. 이 편은 반대로 간다. 원인에서 담당으로, 담당에서 문장으로. 요청은 화를 내는 일이 아니라 이름을 정확히 부르는 일이다.

이 편은 읽는 글이 아니라 꺼내 쓰는 글입니다

앞의 여덟 편은 각각 하나의 증상 계통을 파고들었습니다. 이 편은 그 결과물을 다시 배열합니다.

실무에서 필요한 순간은 대체로 이렇습니다. 제보가 하나 올라왔고, 재현은 됐고, 이제 티켓을 어디로 보낼지 정해야 합니다. 그 판단이 틀리면 웹 팀이 이틀 동안 고칠 수 없는 것을 고치려 하거나, 앱 팀이 자기 코드에 없는 버그를 찾습니다.

그래서 이 편은 세 개의 산출물로 되어 있습니다.

하나. 증상에서 담당으로 가는 매핑표: 여섯 계통으로 묶었습니다. 검색해서 자기 증상을 찾고 담당 열만 보면 됩니다.

둘. 이슈에 담을 재현 정보 양식: 복사해서 티켓 템플릿에 붙이면 됩니다.

셋. 프로젝트 초기에 합의할 설정 체크리스트: 착수 시점에 한 번 돌면 이 시리즈의 절반이 사전에 걸러집니다.


담당은 증상이 아니라 반응으로 가릅니다

먼저 판정 기준을 세워둡니다. 이 기준이 없으면 매핑표는 그냥 긴 목록입니다.

담당을 가르는 질문은 “누구 잘못인가”가 아닙니다. 무엇을 바꾸면 동작이 바뀌는가입니다. 순서가 있습니다.

첫째, 브라우저에서도 재현되는가: 재현되면 웹뷰 이슈가 아닙니다. 일반 웹 이슈로 분류하고 이 시리즈를 덮으면 됩니다.

둘째, 웹 코드를 바꾸면 동작이 바뀌는가: 바뀌면 웹 담당입니다. 앱을 기다릴 이유가 없습니다.

셋째, 앱 설정을 바꾸면 동작이 바뀌는가: 바뀌면 앱 담당입니다. 여기서 웹이 할 일은 구현이 아니라 정확한 요청입니다.

넷째, 둘 다 바꿔야 하는가: 공동입니다. 이 칸이 가장 위험합니다. 한쪽만 고치면 증상이 그대로 남고, 그러면 “고쳤는데 안 됩니다”라는 두 번째 티켓이 열립니다.

여기에 다섯 번째 칸이 하나 더 있습니다. 양쪽 다 못 고치는 것. 엔진이 구현하지 않았거나, 엔진 버전이 고정된 기기에 걸린 항목입니다. 이슈에 “앱 수정 불가, 웹에서 우회함”이라고 적어두면 같은 티켓이 두 번 돌지 않습니다.

웹뷰 이슈의 담당을 판정하고 계통별로 배치한 매트릭스 다이어그램. 다이어그램은 왼쪽의 판정 사다리와 오른쪽의 배치 매트릭스 두 부분으로 구성된다. 왼쪽 판정 사다리는 위에서 아래로 네 개의 마름모 분기가 세로로 이어진다. 최상단 입구 상자에는 '앱에서 재현된 증상'이라고 적혀 있다. 첫 번째 마름모는 '브라우저에서도 재현되는가'이고 예 갈래는 왼쪽으로 빠져나가 회색 상자 '일반 웹 이슈: 이 시리즈 범위 밖'으로 간다. 아니오 갈래는 아래로 이어진다. 두 번째 마름모는 '웹 코드를 바꾸면 동작이 바뀌는가'이고 예 갈래는 오른쪽으로 나가 파란 상자 '웹 담당'으로 간다. 세 번째 마름모는 '앱 설정을 바꾸면 동작이 바뀌는가'이고 예 갈래는 오른쪽으로 나가 주황 상자 '앱 담당'으로 간다. 네 번째 마름모는 '양쪽을 함께 바꿔야 하는가'이고 예 갈래는 오른쪽으로 나가 보라 상자 '공동 담당'으로, 아니오 갈래는 아래로 내려가 회색 빗금 상자 '양쪽 다 못 고침: 엔진 구현 또는 엔진 버전 고정'으로 간다. 오른쪽 배치 매트릭스는 가로 네 개 열과 세로 여섯 개 행으로 된 격자다. 열 제목은 왼쪽부터 '웹 담당(파랑)', '공동 담당(보라)', '앱 담당(주황)', '양쪽 다 못 고침(회색 빗금)'이다. 행 제목은 위에서부터 '진단과 환경 특정(ep.01)', '눌렀는데 반응이 없다(ep.02)', '입력과 폼(ep.03, ep.04)', '화면과 스타일(ep.05, ep.06)', '세션과 저장소(ep.07)', '배포와 수명(ep.08)'이다. 각 칸에는 그 계통의 대표 항목이 짧은 문구로 한두 개씩 들어간다. 진단과 환경 특정 행에서 웹 담당 칸은 '전역 에러 핸들러와 환경 스냅샷', 공동 담당 칸은 '기기별 엔진 버전 특정', 앱 담당 칸은 'isInspectable, setWebContentsDebuggingEnabled', 못 고침 칸은 'Android 10 미만 기기의 고정된 WebView 버전'이다. 눌렀는데 반응이 없다 행에서 웹 담당 칸은 'window.open 반환값 확인, 다운로드를 same-origin HTTPS로', 공동 담당 칸은 'POST 폼 위의 외부 앱 호출, 사용자 제스처 만료', 앱 담당 칸은 'setSupportMultipleWindows, setDownloadListener, onBackPressedDispatcher', 못 고침 칸은 'iOS의 window.print 전달 경로: 공개 API 미확인'이다. 입력과 폼 행에서 웹 담당 칸은 '조합 가드를 값이 아니라 부수 효과에, type=number 제거', 공동 담당 칸은 '인증번호 자동입력, 키보드 표시', 앱 담당 칸은 'onShowFileChooser, setWebAuthenticationSupport', 못 고침 칸은 'Safari 26.x 이하의 조합 이벤트 순서 역전'이다. 화면과 스타일 행에서 웹 담당 칸은 '뷰포트 단위 폴백, 배율을 견디는 레이아웃', 공동 담당 칸은 '키보드 인셋, 안전 영역, 핀치 확대', 앱 담당 칸은 'isLightTheme, windowSoftInputMode, 인셋 전달', 못 고침 칸은 'OS 피커의 외형: 엔진 버전 대기'이다. 세션과 저장소 행에서 웹 담당 칸은 '저장소 실패를 예외로 감싸기, 서비스 워커 없이 성립하는 설계', 공동 담당 칸은 '보안 컨텍스트 의존 기능, 스테이징 접속', 앱 담당 칸은 'file 스킴 대신 WebViewAssetLoader, setDomStorageEnabled', 못 고침 칸은 'iOS 웹뷰의 서비스 워커'다. 배포와 수명 행에서 웹 담당 칸은 '빌드 버전 감지 배너, 만료 시각 비교, 폼 초안 저장', 공동 담당 칸은 '캐시 헤더와 캐시 모드', 앱 담당 칸은 'setCacheMode, onRenderProcessGone', 못 고침 칸은 'Android 웹뷰의 immutable 지시자'다. 판정 사다리의 세 결과 상자에서 매트릭스의 같은 색 열 머리로 각각 얇은 연결선이 그어져 있다. 다이어그램 하단에는 가로로 긴 캡션 띠가 있고 '담당은 증상의 종류가 아니라 무엇을 바꿨을 때 동작이 바뀌는지로 갈린다'라고 적혀 있다.


증상에서 담당으로

여섯 개의 표로 나눴습니다. 편별이 아니라 계통별이라, 한 표 안에 여러 편의 항목이 섞여 있습니다.

담당 표기는 셋입니다. 은 웹 코드만 고치면 됩니다. 은 웹 코드로 대신할 방법이 없습니다. 공동은 양쪽이 함께 움직여야 증상이 사라집니다.

진단과 환경 특정

증상원인담당해당 편
어떤 폰에서는 되고 어떤 폰에서는 안 됩니다Android WebView가 별도 APK로 갱신되어 같은 OS에서도 엔진 버전이 다릅니다공동ep.01
UA에서 기기 모델명과 OS 버전이 안 읽힙니다Android 17부터 기본 UA가 축소되어 고정 문자열로 대체됩니다ep.01
UA 스니핑 라이브러리가 웹뷰를 Chrome으로 분류합니다Android 웹뷰 UA에 Chrome/버전 토큰이 그대로 남아 있습니다ep.01
릴리즈 빌드에 인스펙터가 안 붙습니다isInspectable 기본 false, setWebContentsDebuggingEnabled 기본 falseep.01
서버 로그에서 앱 웹뷰 트래픽을 분리할 수 없습니다UA에 앱 식별 토큰이 없습니다ep.01
운영에서만 재현되는데 콘솔을 볼 수 없습니다프로덕션 디버깅 활성화는 보안 부채로 문서가 명시적으로 말립니다ep.01
구형 기기에서 최신 CSS가 안 먹습니다Android 10 미만은 마지막으로 받은 WebView 버전에 고정됩니다ep.01

마지막 행은 “진짜 안 되는 것”입니다. 앱이 켤 스위치가 없습니다. 웹이 기능 탐지로 대체 경로를 만드는 것 외에 방법이 없어서 담당을 웹으로 뒀습니다.

눌렀는데 아무 일도 안 일어납니다

증상원인담당해당 편
새 창 링크가 무반응입니다Android setSupportMultipleWindows 기본 false, iOS는 uiDelegate 미설정ep.02
window.close()가 화면을 안 닫습니다앱이 onCloseWindow 또는 webViewDidClose 알림을 받고도 뷰를 제거하지 않습니다ep.02
새 창이 안 열리고 현재 페이지가 갈아치워집니다다중 창 지원이 꺼져 있으면 새 창 요청이 같은 웹뷰의 최상위 내비게이션이 됩니다공동ep.02
링크는 되는데 인증 폼만 외부 앱 호출에 실패합니다shouldOverrideUrlLoading은 POST 요청에 호출되지 않습니다공동ep.02
iOS에서 커스텀 스킴이 안 열립니다앱이 UIApplication.open으로 넘기는 연결선이 없습니다ep.02
intent:// 링크가 iOS에서 무반응입니다iOS에 대응 개념 자체가 없습니다ep.02
다운로드가 무반응입니다Android setDownloadListener 미등록, iOS는 .download 정책과 WKDownloadDelegate 부재ep.02
blob: 다운로드만 실패합니다DownloadManager.Request는 HTTP와 HTTPS URI만 받습니다ep.02
파일명이 깨지거나 무시됩니다앱이 contentDisposition 또는 suggestedFilename을 쓰지 않습니다공동ep.02
PDF가 열리기만 하고 저장이 안 됩니다WebKit이 표시 가능한 MIME 타입은 그냥 표시됩니다ep.02
인쇄 버튼이 무반응입니다Android는 createPrintDocumentAdapter 경로 부재, iOS는 공개 API 미확인공동ep.02
뒤로 가기를 눌렀는데 앱이 그냥 닫힙니다onBackPressedDispatcher 콜백이 웹뷰 히스토리에 연결되어 있지 않습니다ep.02
뒤로 갔더니 모달만 남고 화면이 두 칸 뒤로 갔습니다웹이 화면 상태를 히스토리 항목으로 만들지 않았습니다ep.02
iOS에서 스와이프 뒤로 가기가 안 됩니다allowsBackForwardNavigationGestures 기본값 falseep.02
분명히 눌렀는데 팝업으로 오인되어 차단됩니다hasGesture()가 실제 제스처에도 false일 수 있고, 웹은 비동기 뒤에서 창을 엽니다공동ep.02
결제 시트가 안 뜨거나 PaymentRequest 호출이 실패합니다안드로이드는 웹뷰에서 기본 비활성이고, iOS는 Apple Pay가 스크립트 주입 API와 공존하지 못합니다ep.02

입력과 폼

증상원인담당해당 편
파일 선택 버튼이 무반응입니다 (Android)targetSdk 21 이상에서 onShowFileChooser 미구현이면 조용히 끝납니다ep.03
accept가 무시됩니다 (iOS)앱이 runOpenPanelWith를 구현하는 순간 MIME 정보가 전달되지 않습니다공동ep.03
카드번호 입력에서 값이 통째로 사라집니다type="number"의 값 정제 알고리즘이 비수치 입력을 빈 문자열로 만듭니다ep.03
비밀번호 자동완성이 안 붙습니다 (Android)자동완성은 기기에 설치된 Chrome 또는 System WebView 61 이상을 요구합니다공동ep.03
인증번호가 자동으로 안 채워집니다 (Android 웹뷰)WebOTP가 Android WebView에서 지원되지 않습니다공동ep.03
인증번호 자동입력이 어느 플랫폼에서도 안 붙습니다입력을 여섯 칸으로 쪼갰거나 autocomplete="one-time-code"가 없습니다ep.03
focus()를 불렀는데 키보드가 안 올라옵니다웹뷰라는 뷰 자체가 포커스를 받지 못한 상태일 수 있습니다공동ep.03
선택 상자가 OS 피커로 뜹니다폼 컨트롤은 선언과 렌더링이 분리되어 엔진이 플랫폼에 위임합니다ep.03
커스텀 드롭다운이 스크린리더에서 읽히지 않습니다표준 요소가 공짜로 주던 접근성을 직접 구현하지 않았습니다ep.03
전송했는데 마지막 글자가 빠졌습니다isComposing 가드를 값 갱신에 걸었습니다ep.04
엔터가 두 번 실행됩니다조합 확정 엔터와 제출 엔터를 구분하지 않았습니다ep.04
실시간 조회가 한 글자에 서너 번씩 나갑니다조합 중에도 input 이벤트가 매 단계 발생합니다ep.04
iOS에서만 엔터 가드가 뚫립니다Safari 26.x 이하는 조합 확정 키의 이벤트 순서가 역전됩니다ep.04
네이티브 전송 버튼을 누르면 마지막 음절이 누락됩니다브리지 호출 전에 조합이 확정되지 않았습니다ep.04
공용 인풋 컴포넌트에서만 조합 처리가 안 됩니다v-model을 네이티브 요소에 위임하지 않고 직접 풀어 썼습니다ep.04
브라우저에서는 패스키로 로그인되는데 앱에서만 안 됩니다앱과 도메인이 묶여 있어야 열립니다. 안드로이드는 기본값이 꺼짐, iOS는 Associated Domains 미설정ep.03

화면과 스타일

증상원인담당해당 편
키보드가 올라오면 하단 버튼이 가려집니다레이아웃 뷰포트와 시각 뷰포트 중 무엇이 줄어드는지가 환경마다 다릅니다공동ep.05
WebView 139 이상에서 입력 자체가 불가능합니다resize 핸들러에서 포커스를 해제해 무한 루프가 생깁니다ep.05
100vh 컨테이너가 화면보다 깁니다vh는 가장 큰 뷰포트인 lvh와 같습니다ep.05
안전 영역 값이 항상 0으로 내려옵니다Android WebView는 M136과 M144를 기점으로 인셋을 전달하고, 앱이 이미 소비했을 수도 있습니다ep.05
상단 여백이 두 배로 벌어집니다네이티브 컨테이너와 CSS가 같은 인셋을 두 번 넣습니다ep.05
뷰포트 메타의 width가 반영되지 않습니다setUseWideViewPortfalse면 메타 태그의 너비가 쓰이지 않습니다ep.05
가로 캐러셀을 밀면 뒤로가기가 걸립니다네이티브 제스처 인식기는 CSS 바깥에 있습니다공동ep.05
목록 상단에서 당기면 새로고침되어 초기화됩니다앱이 웹뷰를 새로고침 컨테이너 안에 넣어두었습니다ep.05
약관 박스에 더 읽을 내용이 있다는 신호가 없습니다scrollbar-width가 Android WebView에서 미지원으로 기록되어 있습니다ep.05
sticky 헤더가 안 붙습니다조상의 overflow가 스크롤 메커니즘을 만들었거나 인셋 프로퍼티가 비어 있습니다ep.05
비디오가 전체화면으로 튀어나옵니다allowsInlineMediaPlayback의 iPhone 기본값이 false이고 playsinline도 필요합니다공동ep.05
기기는 다크인데 웹 화면만 라이트입니다Android WebView의 prefers-color-scheme는 앱 테마의 android:isLightTheme가 정합니다ep.06
브랜드 색이 회색으로 뭉개집니다알고리즘 다크닝이 켜져 있고 페이지에 color-scheme 선언이 없습니다공동ep.06
바운스 영역만 다른 색으로 번쩍입니다루트 배경색이 없거나 underPageBackgroundColor가 다릅니다공동ep.06
글자 크기가 기기마다 다릅니다Android WebView가 시스템 글꼴 배율을 웹 콘텐츠에 곱합니다ep.06
핀치 확대가 안 됩니다Android는 setBuiltInZoomControls 기본 false, iOS는 페이지의 확대 제한을 그대로 따릅니다공동ep.06
CSS로 막았는데 롱프레스 메뉴가 뜹니다최종 결정권이 WKUIDelegate의 컨텍스트 메뉴 메서드에 있습니다ep.06
한글 볼드가 유독 뭉개집니다폴백으로 도착한 폰트에 웨이트가 없어 합성됩니다ep.06
z-index: 9999인데 네이티브 헤더에 가려집니다웹의 쌓임 순서는 웹뷰 내부에서만 유효합니다ep.06
떨림을 잡으려 넣은 코드가 하단 고정 버튼을 망가뜨렸습니다조상의 transform이나 will-change가 새 컨테이닝 블록을 만듭니다ep.06

세션과 저장소

증상원인담당해당 편
브라우저에서는 로그인이 유지되는데 앱에서는 다시 로그인입니다웹뷰의 저장소는 브라우저가 아니라 앱이 소유합니다. 정상 동작입니다ep.07
앱을 껐다 켜면 세션이 사라집니다앱이 비영속 데이터 스토어를 붙였을 수 있습니다ep.07
localStorage가 아예 없습니다Android setDomStorageEnabled 기본값이 false입니다ep.07
localStorage 접근이 SecurityError로 죽습니다file:// 로드로 불투명 오리진이 부여되었습니다ep.07
CORS 허용 목록에 도메인을 추가해도 통과하지 못합니다불투명 오리진은 직렬화하면 null이라 등록할 도메인이 없습니다ep.07
복사 버튼이 아무 일도 안 합니다클립보드 API는 보안 컨텍스트를 요구하고, 읽기는 사용자 활성화도 요구합니다공동ep.07
여러 도메인을 오가는 인증이 실패합니다targetSdk 21 이상은 서드파티 쿠키 차단이 기본입니다ep.07
결제 화면에서만 로그인이 풀립니다별도 프로세스의 WebView는 쿠키 저장소를 직접 공유하지 못합니다ep.07
카메라 권한이 프롬프트 없이 거부됩니다onPermissionRequest를 오버라이드하지 않으면 무조건 거부입니다ep.07
위치 권한 프롬프트가 아예 안 뜹니다API 23 초과를 타깃하면 비보안 오리진의 위치 요청은 자동 거부됩니다공동ep.07
스테이징 주소를 넣었더니 흰 화면입니다앱 전송 보안, 혼합 콘텐츠 정책, 인증서 검증 세 층 중 하나에 걸립니다공동ep.07
서비스 워커 등록이 실패합니다 (iOS)Apple이 WKWebView에서의 지원을 문서화한 적이 없습니다ep.07
웹 화면의 접근성이 전혀 읽히지 않습니다앱이 웹뷰 자체를 접근성에서 감췄을 수 있습니다ep.07
브라우저에서 발급받은 인증서가 앱 웹뷰에 안 보입니다브라우저 웹저장소에 있는 인증서와 앱이 소유한 웹뷰 저장소가 분리되어 있습니다공동ep.07

배포와 수명

증상원인담당해당 편
어제 배포했는데 앱에서는 그제 화면입니다앱의 캐시 모드가 만료된 사본을 계속 쓰거나 응답에 Cache-Control이 없습니다공동ep.08
서버에 무엇을 배포해도 화면에 닿지 않습니다앱이 웹 자산을 번들에서 서빙하고 있습니다ep.08
다섯 화면 채운 신청 폼이 처음으로 돌아갑니다렌더러 프로세스가 회수되고, saveState는 표시 데이터를 저장하지 않습니다ep.08
웹 화면을 보다가 앱이 통째로 죽습니다onRenderProcessGone을 구현하지 않으면 앱이 크래시하거나 킬됩니다ep.08
백그라운드에 다녀오면 화면 초기화가 유독 잦습니다렌더러 우선순위 정책이 기본값에서 내려가 있을 수 있습니다ep.08
세션 카운트다운이 서버와 어긋납니다백그라운드 스로틀링과 앱의 전역 타이머 정지가 겹칩니다ep.08
뒤로 가기 하면 폼이 날아갑니다응답의 no-store가 뒤로가기 캐시를 막습니다ep.08
지하 주차장에서 앱을 켜면 흰 화면입니다첫 로드 실패 화면은 앱의 오류 콜백이 그립니다ep.08
통신이 한 번 실패하면 복구할 방법이 없습니다웹뷰에는 새로고침 버튼이 없고 화면에 재시도 수단이 없습니다ep.08

이슈에 담아야 할 것

담당을 갈랐으면 다음은 문장입니다. 티켓의 질이 왕복 횟수를 정합니다.

“갤럭시에서 안 돼요”로 시작하는 티켓은 재현에만 이틀이 걸립니다. 아래 항목이 채워져 있으면 앱 개발자가 첫 답장에서 설정 이름을 말할 수 있습니다.

[증상]
한 문장으로. 무엇을 눌렀고 무엇을 기대했고 실제로 무엇이 일어났는가.

[재현 경로]
1.
2.
3.
재현율:  (항상 / 가끔 — 몇 번 중 몇 번)

[브라우저 재현 여부]
같은 URL을 기기의 브라우저에서 열었을 때:  (재현됨 / 재현 안 됨 / 확인 못 함)
※ 재현되면 웹뷰 이슈가 아닙니다. 이 항목을 가장 먼저 채웁니다.

[환경]
기기 모델:
OS 버전:
WebView 버전:      (Android만. WebView DevTools의 Home 화면에서 읽습니다)
앱 빌드 번호:
웹 배포 식별자:      (화면에 심어둔 build 값)
접속 URL:

[진단 출력]
진단 패널 캡처 또는 환경 스냅샷 JSON 첨부

[담당 추정]
웹 / 앱 / 공동 / 미상
근거:            (웹 코드를 이렇게 바꿔봤더니 동작이 바뀌지 않았음 등)

[요청 지점]
앱에 요청하는 경우 설정 또는 콜백 이름을 적습니다.
예) WebSettings.setSupportMultipleWindows 값 확인 요청

각 항목이 왜 필요한지 짚어둡니다.

브라우저 재현 여부가 첫 번째인 이유: 이 한 줄이 채워져 있으면 티켓의 절반이 그 자리에서 정리됩니다. 재현되면 웹뷰와 무관한 일반 웹 이슈입니다.

WebView 버전이 별도 항목인 이유: Android는 OS 버전을 알아도 엔진 버전을 알 수 없습니다. 같은 Android 14 기기 두 대의 WebView 버전이 다를 수 있습니다. 읽는 방법은 두 가지입니다. 웹 개발자와 QA는 WebView DevTools를 열면 되고, 앱은 코드에서 현재 WebView 패키지 정보를 읽어 로그에 남길 수 있습니다.

# WebView DevTools 실행. Home 화면에 버전 정보가 있습니다
adb shell am start -a "com.android.webview.SHOW_DEV_UI"

Android 16 이상에서 개발자 모드가 켜져 있다면 설정 앱에서도 들어갑니다.

설정 > 시스템 > 개발자 옵션 > WebView DevTools

iOS에는 이 칸이 비어도 됩니다. 엔진이 OS 버전에 묶여 있어 OS 버전이 곧 엔진 버전입니다. 이 비대칭 자체가 두 플랫폼의 구조 차이를 그대로 보여줍니다.

웹 배포 식별자가 필요한 이유: 앱 캐시 때문에 구버전 화면이 도는 상황을 이 한 줄로 가릅니다. 배포 식별자가 어제 값이면 그 티켓은 기능 버그가 아니라 캐시 이슈입니다. 화면에 심어두는 방법은 ep.08에 있습니다.

담당 추정에 근거를 붙이는 이유: “앱 문제인 것 같습니다”와 “웹에서 이 값을 바꿔봤는데 동작이 바뀌지 않았습니다”는 받는 쪽의 반응이 다릅니다. 후자는 확인 항목을 특정해줍니다.

환경 스냅샷은 화면에서 자동으로 만들 수 있습니다. 쿼리스트링으로 열리는 진단 패널을 만들어두면 QA가 캡처 한 장으로 보고합니다. 그 구현은 ep.01에, 오리진 경계를 함께 찍는 스니펫은 ep.07에 있습니다. 다만 이 패널에 개인정보나 세션 값은 절대 담지 않습니다.


착수 시점에 한 번 돌면 되는 체크리스트

여기까지가 사후 대응입니다. 이 절은 사전 대응입니다.

아래 항목은 프로젝트 초기에 앱 팀과 한 번 맞춰두면 되는 것들입니다. 대부분 한 줄짜리 설정이고, 나중에 이슈로 올라왔을 때보다 착수 시점에 합의하는 편이 훨씬 편합니다.

Android

항목종류기본값왜 필요한가해당 편
WebSettings.setJavaScriptEnabled()WebSettings 메서드false이게 꺼져 있으면 아무것도 시작되지 않습니다ep.00
WebSettings.setDomStorageEnabled() (API 7+)WebSettings 메서드falselocalStoragesessionStorage가 없는 물건이 됩니다ep.07
WebSettings.setSupportMultipleWindows() + WebChromeClient.onCreateWindow()설정 + 콜백false꺼져 있으면 새 창 요청이 현재 페이지를 갈아치웁니다ep.02
WebSettings.setJavaScriptCanOpenWindowsAutomatically()WebSettings 메서드false사용자 제스처 없는 창 요청이 조용히 실패합니다ep.02
WebView.setDownloadListener() 등록WebView 메서드없음등록하지 않으면 다운로드를 받을 곳이 없습니다ep.02
WebChromeClient.onShowFileChooser() 구현WebChromeClient 콜백미구현targetSdk 21 이상에서 첨부 버튼이 무반응이 됩니다ep.03
WebChromeClient.onPermissionRequest() 구현WebChromeClient 콜백미구현오버라이드하지 않으면 카메라·마이크 권한이 무조건 거부됩니다ep.07
WebChromeClient.onGeolocationPermissionsShowPrompt() 구현WebChromeClient 콜백미구현위치 권한 프롬프트가 뜰 자리가 없습니다ep.07
WebViewClient.onRenderProcessGone() 구현WebViewClient 콜백미구현렌더러가 죽으면 앱이 크래시하거나 킬됩니다ep.08
WebSettings.setCacheMode()WebSettings 메서드LOAD_DEFAULTLOAD_CACHE_ELSE_NETWORK이면 만료된 사본이 계속 쓰입니다ep.08
WebSettings.setMixedContentMode() (API 21+)WebSettings 메서드targetSdk 21+면 MIXED_CONTENT_NEVER_ALLOW스테이징에서 평문 리소스가 차단되는 원인입니다ep.07
WebSettings.setAllowFileAccess() (API 3+)WebSettings 메서드targetSdk 30+면 falsetargetSdk를 올렸더니 갑자기 안 되는 항목의 대표입니다ep.07
androidx.webkit.WebViewAssetLoader 사용 여부androidx 클래스미사용file:// 로드는 오리진을 불투명하게 만들어 저장소·CORS·보안 컨텍스트를 함께 무너뜨립니다ep.07
WebSettings.setBuiltInZoomControls()WebSettings 메서드false꺼져 있으면 핀치 확대 자체가 비활성입니다. 접근성 기준과 직결되니 true로 요청합니다ep.06
WebSettings.setDisplayZoomControls()WebSettings 메서드true기본값 그대로 두면 화면 위에 플러스 마이너스 버튼이 뜹니다. 버튼 없이 핀치만 얻으려면 false로 요청합니다ep.06
WebSettings.setTextZoom()WebSettings 메서드100앱이 한 번이라도 호출하면 시스템 글꼴 배율 동기화가 끊깁니다ep.06
WebSettings.setAlgorithmicDarkeningAllowed() (API 33+)WebSettings 메서드false켜져 있으면 색이 알고리즘으로 반전됩니다ep.06
앱 테마의 android:isLightTheme테마 속성미지정이면 light 취급웹뷰의 prefers-color-scheme 값을 이 속성이 정합니다ep.06
WebSettings.setUseWideViewPort()WebSettings 메서드문서에 명시 없음false면 뷰포트 메타의 width가 쓰이지 않습니다ep.05
android:windowSoftInputMode매니페스트 속성미지정키보드가 떴을 때 레이아웃 반응을 매니페스트가 정합니다ep.05
인셋 전달 방식설계 선택앱 구현에 따름이미 소비한 인셋을 0으로 만들어 내려보내야 여백이 두 번 들어가지 않습니다ep.05
WebSettings.setUserAgentString() 사용 여부와 최종 문자열WebSettings 메서드기본 UA앱 식별 토큰이 있어야 서버 로그에서 웹뷰 트래픽이 분리됩니다ep.01
WebView.setWebContentsDebuggingEnabled()WebView 정적 메서드false개발·스테이징 빌드에서 인스펙터를 명시적으로 켜는 경로입니다. WebView 113 이상이고 매니페스트에 android:debuggable="true"면 호출 없이도 켜집니다ep.01
WebChromeClient.onConsoleMessage() 구현WebChromeClient 콜백미구현웹 로그를 네이티브 로그 타임라인에 합칩니다ep.01
WebView.getCurrentWebViewPackage() 로깅 (API 26+)WebView 정적 메서드없음이슈 리포트에 WebView 버전을 자동으로 담습니다ep.01

소속을 붙여 둔 이유가 있습니다. 이름이 비슷해도 어디에 있는지가 제각각입니다. 대부분은 WebSettings의 메서드인데 setDownloadListener()setWebContentsDebuggingEnabled()WebView 쪽이고, WebViewAssetLoader는 플랫폼이 아니라 androidx 라이브러리의 클래스입니다. 요청할 때 클래스까지 짚으면 되묻는 왕복이 한 번 줄어듭니다.

마지막 세 항목은 빌드 종류를 명시해서 요청합니다. 디버깅 활성화는 Android 공식 문서가 보안 부채로 명시하고 운영 빌드에서 켜지 말라고 적습니다. 그 문장을 근거로 함께 제시하면 “개발·스테이징만”이라는 합의가 빨리 납니다. 콘솔 통로를 열기로 했다면 그 전에 console.log 문장을 감사해서 개인 식별 정보가 흘러가지 않는지부터 확인합니다.

iOS

항목종류기본값왜 필요한가해당 편
WKWebView.uiDelegate 설정과 createWebViewWith 구현WKUIDelegate 메서드미설정새 창 요청을 받을 자리가 없습니다ep.02
WKNavigationDelegate.decidePolicyFor에서 커스텀 스킴 처리WKNavigationDelegate 메서드미구현외부 앱 호출을 앱이 직접 넘겨야 합니다ep.02
.download 정책 반환과 WKDownloadDelegate (iOS 14.5+)정책 열거형 + 프로토콜미구현14.5 미만에는 다운로드 공개 경로 자체가 없습니다ep.02
WKWebView.allowsBackForwardNavigationGestures (iOS 8.0+)WKWebView 프로퍼티false켜면 스와이프 뒤로 가기가 생기고, 가로 캐러셀과 충돌합니다ep.02, ep.05
WKUIDelegate.runOpenPanelWith 구현 여부 (iOS 18.4+)WKUIDelegate 메서드미구현이면 업로드 활성구현하는 순간 accept 정보가 전달되지 않습니다ep.03
WKWebViewConfiguration.websiteDataStore가 영속인지WKWebViewConfiguration 프로퍼티지정 없으면 영속비영속이면 앱 재시작마다 세션이 사라집니다ep.07
loadFileURL 또는 WKURLSchemeHandler 사용 방식설계 선택앱 구현에 따름로컬 번들 로드 방식이 오리진 판정을 좌우합니다ep.07
WKUIDelegate.requestMediaCapturePermissionFor (iOS 15.0+)WKUIDelegate 메서드미구현이면 프롬프트두 번 뜨는 프롬프트를 앱이 대신 결정할 수 있습니다ep.07
NSCameraUsageDescription 등 사용 목적 문자열Info.plist 키없음카메라·위치는 문자열이 없으면 프롬프트 이전에 막힙니다ep.07
WKWebViewConfiguration.ignoresViewportScaleLimits (iOS 10+)WKWebViewConfiguration 프로퍼티false페이지의 확대 제한이 그대로 적용됩니다ep.06
WKWebViewConfiguration.allowsInlineMediaPlayback (iOS 8.0+)WKWebViewConfiguration 프로퍼티iPhone false비디오가 전체화면 컨트롤러로 튀어나옵니다ep.05
WKPreferences.isElementFullscreenEnabled (iOS 15.4+)WKPreferences 프로퍼티false켜면 시스템이 웹뷰를 앱의 뷰 계층에서 제거합니다ep.05
UIScrollView.contentInsetAdjustmentBehaviorUIScrollView 프로퍼티.automatic안전 영역이 콘텐츠 영역에 반영되는 방식이 여기서 정해집니다ep.05, ep.06
UIViewController.additionalSafeAreaInsets (iOS 11.0+)UIViewController 프로퍼티없음네이티브 헤더·탭바의 높이를 웹에 알려주는 통로입니다ep.06
WKWebView.allowsLinkPreview (iOS 9.0+)WKWebView 프로퍼티iOS 10 이상 true길게 누르기를 웹이 다른 용도로 쓰고 있다면 겹칩니다ep.05
WKWebView.underPageBackgroundColor (iOS 15.0+)WKWebView 프로퍼티루트 배경색에서 유도바운스 영역만 다른 색으로 번쩍이는 문제의 지점입니다ep.06
WKWebViewConfiguration.applicationNameForUserAgent (iOS 9.0+)WKWebViewConfiguration 프로퍼티없음UA 접미사로 앱을 식별합니다ep.01
WKWebView.isInspectable (iOS 16.4+)WKWebView 프로퍼티false최신 SDK로 빌드하면 디버그 빌드조차 이 한 줄 없이는 열리지 않습니다ep.01
WKUserContentController.add(_:name:) 설치 여부와 핸들러 이름WKUserContentController 메서드없음웹에서 네이티브로 값을 보내는 공식 통로입니다ep.01

iOS는 웹뷰 바깥에 있는 항목이 섞여 있어 더 헷갈립니다. contentInsetAdjustmentBehaviorWKWebView가 아니라 그 안의 scrollView 프로퍼티이고, additionalSafeAreaInsets는 웹뷰를 담은 뷰 컨트롤러 쪽입니다. 그리고 WKWebView에 직접 있는 것과 생성 시점의 WKWebViewConfiguration에 있는 것이 갈립니다. 후자는 웹뷰를 만든 뒤에는 바꿀 수 없으니 요청 시점이 달라집니다.

한 가지 항목은 목록에 넣지 않았습니다. app-bound domains는 요청하지 않습니다. 이 키를 Info.plist에 추가하면 그 앱의 모든 웹뷰가 스크립트 주입, 커스텀 스타일시트, 쿠키 조작, 메시지 핸들러가 거부되는 모드를 기본으로 하게 됩니다. 얻는 것보다 잃는 것이 큽니다. 근거는 ep.07에 있습니다.

요청하면 안 되는 것

체크리스트에 넣지 말아야 할 항목도 정리해둡니다. 요청 자체가 방향이 틀린 것들입니다.

흔한 요청왜 안 되는가대신 요청할 것
운영 빌드에서 웹 콘텐츠 디버깅 켜기공식 문서가 보안 부채로 명시하고 프로덕션 활성화를 말립니다개발·스테이징 빌드 한정, 그리고 웹 쪽 원격 로그 인프라
사설 인증서 오류 무시Android 문서가 사용자에게 SSL 오류를 묻지 말라고 직접 경고합니다스테이징에 신뢰 가능한 인증서, 또는 개발 빌드 전용 경로
setAllowUniversalAccessFromFileURLs 켜기API 30에서 폐기됐고 문서가 안전하지 않다고 명시합니다WebViewAssetLoader로 오리진 자체를 되돌리기
웹뷰에 하드웨어 가속 켜기뷰 레벨에서는 끌 수만 있고 켤 수 없습니다소프트웨어 레이어로 내려둔 곳이 있는지 확인
레이아웃이 깨지니 setTextZoom(100) 고정사용자의 접근성 설정을 무시하는 선택입니다배율이 곱해져도 무너지지 않는 레이아웃

비어 있는 것은 기능이 아니라 결정입니다

여덟 편을 지나오며 확인해본 항목이 백 개 가까이 됩니다. 그중 앱이 켜면 되는 것이 절반을 넘고, 웹이 다르게 짜면 되는 것이 그다음입니다. 어느 쪽도 손댈 수 없는 것은 조합 이벤트 순서, 구형 기기에 고정된 엔진 버전, iOS 웹뷰의 서비스 워커 정도입니다.

웹뷰가 어려운 이유는 기능이 없어서가 아니라 결정이 비어 있어서입니다. 브라우저 벤더가 채워둔 자리를 웹뷰에서는 팀이 프로젝트마다 다시 채워야 합니다.


참고 자료