여덟 편은 증상에서 출발했다. 이 편은 반대로 간다. 원인에서 담당으로, 담당에서 문장으로. 요청은 화를 내는 일이 아니라 이름을 정확히 부르는 일이다.
이 편은 읽는 글이 아니라 꺼내 쓰는 글입니다
앞의 여덟 편은 각각 하나의 증상 계통을 파고들었습니다. 이 편은 그 결과물을 다시 배열합니다.
실무에서 필요한 순간은 대체로 이렇습니다. 제보가 하나 올라왔고, 재현은 됐고, 이제 티켓을 어디로 보낼지 정해야 합니다. 그 판단이 틀리면 웹 팀이 이틀 동안 고칠 수 없는 것을 고치려 하거나, 앱 팀이 자기 코드에 없는 버그를 찾습니다.
그래서 이 편은 세 개의 산출물로 되어 있습니다.
하나. 증상에서 담당으로 가는 매핑표: 여섯 계통으로 묶었습니다. 검색해서 자기 증상을 찾고 담당 열만 보면 됩니다.
둘. 이슈에 담을 재현 정보 양식: 복사해서 티켓 템플릿에 붙이면 됩니다.
셋. 프로젝트 초기에 합의할 설정 체크리스트: 착수 시점에 한 번 돌면 이 시리즈의 절반이 사전에 걸러집니다.
담당은 증상이 아니라 반응으로 가릅니다
먼저 판정 기준을 세워둡니다. 이 기준이 없으면 매핑표는 그냥 긴 목록입니다.
담당을 가르는 질문은 “누구 잘못인가”가 아닙니다. 무엇을 바꾸면 동작이 바뀌는가입니다. 순서가 있습니다.
첫째, 브라우저에서도 재현되는가: 재현되면 웹뷰 이슈가 아닙니다. 일반 웹 이슈로 분류하고 이 시리즈를 덮으면 됩니다.
둘째, 웹 코드를 바꾸면 동작이 바뀌는가: 바뀌면 웹 담당입니다. 앱을 기다릴 이유가 없습니다.
셋째, 앱 설정을 바꾸면 동작이 바뀌는가: 바뀌면 앱 담당입니다. 여기서 웹이 할 일은 구현이 아니라 정확한 요청입니다.
넷째, 둘 다 바꿔야 하는가: 공동입니다. 이 칸이 가장 위험합니다. 한쪽만 고치면 증상이 그대로 남고, 그러면 “고쳤는데 안 됩니다”라는 두 번째 티켓이 열립니다.
여기에 다섯 번째 칸이 하나 더 있습니다. 양쪽 다 못 고치는 것. 엔진이 구현하지 않았거나, 엔진 버전이 고정된 기기에 걸린 항목입니다. 이슈에 “앱 수정 불가, 웹에서 우회함”이라고 적어두면 같은 티켓이 두 번 돌지 않습니다.
증상에서 담당으로
여섯 개의 표로 나눴습니다. 편별이 아니라 계통별이라, 한 표 안에 여러 편의 항목이 섞여 있습니다.
담당 표기는 셋입니다. 웹은 웹 코드만 고치면 됩니다. 앱은 웹 코드로 대신할 방법이 없습니다. 공동은 양쪽이 함께 움직여야 증상이 사라집니다.
진단과 환경 특정
| 증상 | 원인 | 담당 | 해당 편 |
|---|---|---|---|
| 어떤 폰에서는 되고 어떤 폰에서는 안 됩니다 | Android WebView가 별도 APK로 갱신되어 같은 OS에서도 엔진 버전이 다릅니다 | 공동 | ep.01 |
| UA에서 기기 모델명과 OS 버전이 안 읽힙니다 | Android 17부터 기본 UA가 축소되어 고정 문자열로 대체됩니다 | 웹 | ep.01 |
| UA 스니핑 라이브러리가 웹뷰를 Chrome으로 분류합니다 | Android 웹뷰 UA에 Chrome/버전 토큰이 그대로 남아 있습니다 | 웹 | ep.01 |
| 릴리즈 빌드에 인스펙터가 안 붙습니다 | isInspectable 기본 false, setWebContentsDebuggingEnabled 기본 false | 앱 | ep.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 기본값 false | 앱 | ep.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가 반영되지 않습니다 | setUseWideViewPort가 false면 메타 태그의 너비가 쓰이지 않습니다 | 앱 | 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 메서드 | false | localStorage와 sessionStorage가 없는 물건이 됩니다 | 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_DEFAULT | LOAD_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+면 false | targetSdk를 올렸더니 갑자기 안 되는 항목의 대표입니다 | 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.contentInsetAdjustmentBehavior | UIScrollView 프로퍼티 | .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는 웹뷰 바깥에 있는 항목이 섞여 있어 더 헷갈립니다. contentInsetAdjustmentBehavior는 WKWebView가 아니라 그 안의 scrollView 프로퍼티이고, additionalSafeAreaInsets는 웹뷰를 담은 뷰 컨트롤러 쪽입니다. 그리고 WKWebView에 직접 있는 것과 생성 시점의 WKWebViewConfiguration에 있는 것이 갈립니다. 후자는 웹뷰를 만든 뒤에는 바꿀 수 없으니 요청 시점이 달라집니다.
한 가지 항목은 목록에 넣지 않았습니다. app-bound domains는 요청하지 않습니다. 이 키를 Info.plist에 추가하면 그 앱의 모든 웹뷰가 스크립트 주입, 커스텀 스타일시트, 쿠키 조작, 메시지 핸들러가 거부되는 모드를 기본으로 하게 됩니다. 얻는 것보다 잃는 것이 큽니다. 근거는 ep.07에 있습니다.
요청하면 안 되는 것
체크리스트에 넣지 말아야 할 항목도 정리해둡니다. 요청 자체가 방향이 틀린 것들입니다.
| 흔한 요청 | 왜 안 되는가 | 대신 요청할 것 |
|---|---|---|
| 운영 빌드에서 웹 콘텐츠 디버깅 켜기 | 공식 문서가 보안 부채로 명시하고 프로덕션 활성화를 말립니다 | 개발·스테이징 빌드 한정, 그리고 웹 쪽 원격 로그 인프라 |
| 사설 인증서 오류 무시 | Android 문서가 사용자에게 SSL 오류를 묻지 말라고 직접 경고합니다 | 스테이징에 신뢰 가능한 인증서, 또는 개발 빌드 전용 경로 |
setAllowUniversalAccessFromFileURLs 켜기 | API 30에서 폐기됐고 문서가 안전하지 않다고 명시합니다 | WebViewAssetLoader로 오리진 자체를 되돌리기 |
| 웹뷰에 하드웨어 가속 켜기 | 뷰 레벨에서는 끌 수만 있고 켤 수 없습니다 | 소프트웨어 레이어로 내려둔 곳이 있는지 확인 |
레이아웃이 깨지니 setTextZoom(100) 고정 | 사용자의 접근성 설정을 무시하는 선택입니다 | 배율이 곱해져도 무너지지 않는 레이아웃 |
비어 있는 것은 기능이 아니라 결정입니다
여덟 편을 지나오며 확인해본 항목이 백 개 가까이 됩니다. 그중 앱이 켜면 되는 것이 절반을 넘고, 웹이 다르게 짜면 되는 것이 그다음입니다. 어느 쪽도 손댈 수 없는 것은 조합 이벤트 순서, 구형 기기에 고정된 엔진 버전, iOS 웹뷰의 서비스 워커 정도입니다.
웹뷰가 어려운 이유는 기능이 없어서가 아니라 결정이 비어 있어서입니다. 브라우저 벤더가 채워둔 자리를 웹뷰에서는 팀이 프로젝트마다 다시 채워야 합니다.
참고 자료
- Apple. Supporting associated domains. Apple Developer Documentation. https://developer.apple.com/documentation/xcode/supporting-associated-domains
- Apple. ASWebAuthenticationSession. Apple Developer Documentation. https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession
- Apple. WKWebViewConfiguration. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebviewconfiguration
- Apple. WKPreferences. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkpreferences
- Apple. WKWebpagePreferences.allowsContentJavaScript. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebpagepreferences/allowscontentjavascript
- Apple. WKWebView.isInspectable. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebview/isinspectable
- Apple. WKWebView.allowsLinkPreview. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebview/allowslinkpreview
- Apple. WKWebsiteDataStore. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebsitedatastore
- Android Developers. WebSettings. https://developer.android.com/reference/android/webkit/WebSettings
- Android Developers. WebView. https://developer.android.com/reference/android/webkit/WebView
- Android Developers. WebSettingsCompat (androidx.webkit). https://developer.android.com/reference/androidx/webkit/WebSettingsCompat
- Android Developers. WebViewFeature (androidx.webkit). https://developer.android.com/reference/androidx/webkit/WebViewFeature
- Android Developers. Authenticate users with WebView. https://developer.android.com/identity/sign-in/credential-manager-webview
- Android Developers. Debug using WebView DevTools App. https://developer.android.com/develop/ui/views/layout/webapps/debug-webview-devtools-app
- Android Developers Blog. User-Agent Reduction on Android WebView. https://android-developers.googleblog.com/2024/12/user-agent-reduction-on-android-webview.html
- Chrome Releases. Chrome for Android Update (136.0.7103.60, 2025-04-29). https://chromereleases.googleblog.com/2025/04/chrome-for-android-update\_29.html
- MDN Web Docs. Payment Request API. https://developer.mozilla.org/en-US/docs/Web/API/Payment\_Request\_API
- MDN. browser-compat-data (api/PaymentRequest.json, api/PublicKeyCredential.json, api/CredentialsContainer.json). https://github.com/mdn/browser-compat-data