같은 웹뷰가 아니다. 한쪽은 OS를 따라 올라가고, 다른 쪽은 앱처럼 따로 올라간다. 그래서 “특정 기기에서만”이라는 말이 나온다. 진단은 그 기기를 특정하는 데서 시작한다.
특정 기기에서만 안 됩니다
가장 곤란한 제보의 형태가 있습니다.
“고객센터에 접수된 건인데요, 어떤 폰에서는 되고 어떤 폰에서는 안 된대요.”
이 문장에는 진단에 필요한 정보가 하나도 없습니다. 기기 모델명을 받아와도 절반밖에 안 됩니다. 같은 모델에서도 웹뷰 버전이 다를 수 있기 때문입니다.
ep.00에서 웹뷰는 같은 렌더링 엔진에 앱이 조립한 껍데기를 얹은 것이라고 정리했습니다. 그런데 “같은 엔진”이라는 말에는 조건이 붙습니다. 엔진이 어떤 경로로 갱신되는지가 두 플랫폼에서 완전히 다릅니다. 이 차이를 모르면 “왜 이 폰에서만”이라는 질문에 영원히 답할 수 없습니다.
이 편이 답하는 질문은 하나입니다. 내 화면이 지금 어떤 웹뷰에서 돌고 있는가. 증상은 다음 편부터 다룹니다. 여기서는 그 증상을 관찰할 도구를 붙입니다.
iOS는 엔진이 OS에 묶여 있습니다
iOS에서 웹을 띄우는 클래스는 주로 넷입니다. 이름을 먼저 정리해두면 앱 개발자와 대화가 짧아집니다.
| 클래스 | 상태 | 성격 |
|---|---|---|
UIWebView | iOS 2.0 도입, iOS 12.0에서 폐기(deprecated) | 신규 사용 대상이 아닙니다 |
WKWebView | iOS 8.0 도입 | 앱이 내용을 제어하는 인앱 웹뷰입니다 |
SFSafariViewController | iOS 9.0 도입 | 앱이 내용에 접근할 수 없는 별도 웹 인터페이스입니다 |
ASWebAuthenticationSession | iOS 12.0 도입 | 웹 서비스로 사용자를 인증하는 전용 브라우저 세션입니다 |
UIWebView는 Apple 문서에 폐기 시점이 12.0으로 명시되어 있고, 안내문은 WKWebView를 쓰라고 합니다. App Store 정책 쪽에서는 2020년 10월 공지에서 “UIWebView를 포함한 신규 앱은 이미 받지 않는다”고 밝혔고, 앱 업데이트 마감일은 그 시점에 2020년 말 이후로 연기된 뒤 새 날짜가 공지되지 않았습니다. 지금 새로 만드는 화면이 UIWebView 위에서 도는 일은 사실상 없지만, 오래된 앱을 인수인계받았다면 확인해볼 값어치는 있습니다.
SFSafariViewController는 성격이 다릅니다. Apple 문서는 “웹 인터페이스와의 상호작용이 앱에 보이지 않으며, 자동 완성 데이터·방문 기록·웹사이트 데이터에 접근할 수 없다”고 명시합니다. 내용을 손대거나 제어할 필요가 있으면 WKWebView를 쓰라고 안내합니다. 앱이 자식 뷰 컨트롤러로 끼워 넣는 것도 금지되어 있고 모달로만 띄웁니다. 화면이 앱 UI 안에 자연스럽게 박혀 있으면 WKWebView, 위에서 덮여 올라오면 SFSafariViewController일 가능성이 높습니다. 후자라면 앱이 손댈 수 있는 것도 거의 없습니다. 덮여 올라온 화면이 OAuth나 외부 인증 플로우라면 ASWebAuthenticationSession일 수도 있습니다. 사용자 인증이 필요한 서비스에서 흔한 경로이고, 이 셋은 인스펙터를 붙일 때 요청할 대상이 서로 다릅니다.
엔진은 OS 업데이트로만 올라갑니다
Apple의 Safari 릴리즈 노트를 열면 배포 대상 문장이 있습니다. Safari 26.6 릴리즈 노트는 이렇게 적혀 있습니다.
Safari 26.6 is available for iOS 26.6, iPadOS 26.6, visionOS 26.6,
macOS 26.6, macOS Sequoia, and macOS Sonoma.
읽는 법이 있습니다. macOS 쪽에는 Sequoia, Sonoma 같은 이전 OS 이름이 함께 나열됩니다. iOS 쪽에는 26.6이라는 같은 번호의 OS만 나열됩니다. 즉 iOS에서 새 엔진을 받으려면 OS를 올려야 합니다. 다른 경로가 없습니다.
이 노트가 무엇을 포괄하는지는 릴리즈 노트 인덱스 페이지에 적혀 있습니다. Safari, Safari View Controller, 그리고 WKWebView가 한 릴리즈 노트로 묶여 배포됩니다. 이 시리즈의 “같은 엔진”이라는 말의 1차 근거가 여기입니다. iOS에서 Safari와 웹뷰의 엔진 버전이 갈리는 일은 없습니다. OS 버전만 알면 엔진 버전이 결정됩니다.
한 가지 조건이 붙습니다. App Store 심사 지침 2.5.6은 웹을 탐색하는 앱이 WebKit 프레임워크와 WebKit JavaScript를 쓰도록 요구합니다. 다만 EU와 일본을 대상으로 별도 entitlement를 신청하면 대체 엔진을 쓸 수 있고, 인앱 브라우징용으로는 Embedded Browser Engine Entitlement가 따로 있습니다. 대체 엔진 기능은 EU 사용자 기준 iOS 17.4 / iPadOS 18 이상에서 제공됩니다. 국내 앱에는 실무상 무관하지만, “iOS는 무조건 WebKit”이라고 단정하면 사실이 아닙니다.
Android는 웹뷰가 앱처럼 업데이트됩니다
Android 4.4(API 19)에서 WebView 구현이 Chromium 기반으로 통째로 교체되었습니다. Android 공식 문서는 이를 “완전히 새로운 구현”이라고 표현합니다. 여기까지는 iOS와 비슷한 이야기입니다. 갈라지는 지점은 그다음입니다.
Android 5.0(Lollipop)부터 WebView는 프레임워크의 일부가 아니라 별도 APK입니다. Chromium 공식 문서의 문장이 정확합니다. “원래 WebView는 Android 프레임워크의 일부였지만, Android 5.0 이후로는 별도 APK가 WebView 구현을 제공한다. 이 APK는 기기에 선탑재되며 일반 애플리케이션과 같은 방식으로 업데이트된다.”
이 한 문장에 파편화의 원인이 다 들어 있습니다. 웹뷰 버전이 OS 버전과 분리되어 움직입니다. 같은 Android 14 기기 두 대의 웹뷰 버전이 다를 수 있습니다.
어느 패키지가 웹뷰를 제공하는가
시기에 따라 기본 제공자가 다릅니다. Chromium 공식 문서의 허용 패키지 목록을 GMS 기기 기준으로 추리면 이렇습니다.
| Android 버전 | 기본 제공 패키지 | 성격 |
|---|---|---|
| 5.0 ~ 6.x (L-M) | com.google.android.webview | 독립 APK |
| 7.0 ~ 9.x (N-P), 일반 기기 | com.android.chrome | Chrome APK가 웹뷰를 함께 제공 |
| 7.0 ~ 9.x (N-P), TV·차량 | com.google.android.webview | 독립 APK |
| 10 이상 (Q+) | com.google.android.webview | 독립 APK, 채널별 병행 설치 가능 |
Android 7~9 구간이 특이합니다. Chromium 문서는 이 시기의 빌드를 Monochrome이라고 부르며, “Android N-P에서 WebView는 Chrome APK의 일부로 빌드된다”고 설명합니다. Chrome이 이미 채널별 병행 설치를 지원하고 있었기 때문에, 개발자 옵션에 어떤 Chrome을 웹뷰로 쓸지 고르는 메뉴가 추가되었습니다. 그리고 Monochrome은 Trichrome 도입에 따른 변경으로 Android Q 이상과는 호환되지 않습니다.
그래서 “안드로이드 웹뷰는 크롬이다”라는 흔한 문장은 시기에 따라 참이거나 거짓입니다. 7~9의 일반 GMS 기기에서만 참입니다. 조건 없이 쓰면 틀립니다.
Android 7.0(API 24)부터는 사용자가 제공 패키지를 고를 수 있습니다. Android 공식 문서에 명시되어 있고, 개발자 옵션의 “WebView implementation” 메뉴가 그 지점입니다. Q 이상에서는 Stable·Beta·Dev·Canary 네 채널의 WebView를 Play 스토어에서 각각 설치해 동시에 둘 수 있습니다. 테스터의 기기가 어느 날 갑자기 Beta 채널로 바뀌어 있는 상황이 제도적으로 가능하다는 뜻입니다.
구형 OS는 버전이 고정됩니다
Chromium 공식 문서는 현재 상태를 이렇게 적고 있습니다. “현재 우리는 새 WebView 업데이트를 Android Q 이상에 대해서만 지원한다.”
즉 Android 10 미만 기기는 마지막으로 받은 WebView 버전에 고정됩니다. 그 기기에서 최신 CSS 기능이 안 먹는 것은 앱 설정 문제가 아닙니다. 엔진이 거기서 멈춰 있는 것입니다. 이 편의 관통 명제인 “안 켠 것”에 해당하지 않는, 몇 안 되는 진짜 “안 되는 것”입니다.
리테일 기기에서 웹뷰 구현을 임의로 교체할 수도 없습니다. Chromium 문서는 그 이유를 보안으로 설명합니다. 앱이 WebView를 쓰면 WebView 구현 코드가 그 앱의 프로세스 안으로 직접 로드되고, 앱의 메모리와 디스크 데이터, 그리고 앱이 가진 모든 Android 권한에 접근할 수 있게 됩니다. 그래서 AOSP 프레임워크는 시스템 통합자가 지정한 APK만 웹뷰 구현으로 허용합니다. 커스텀 웹뷰를 넣으려면 시스템 이미지 단계에서 해야 합니다.
사용자 에이전트에서 읽어도 되는 것과 안 되는 것
사용자 에이전트(User-Agent) 는 웹 개발자가 가장 먼저 손을 뻗는 신호입니다. 그리고 웹뷰에서 가장 자주 배신하는 신호이기도 합니다. 무엇이 남아 있고 무엇이 무너졌는지를 갈라두면 쓸모가 있습니다.
신뢰할 수 있는 것: Android의 wv 토큰
Android WebView는 UA 플랫폼 토큰에 wv를 포함합니다. Android 공식 블로그가 이를 명시적으로 보증합니다. “사이트는 계속해서 User-Agent 문자열의 wv 토큰을 찾아볼 수 있다. 애플리케이션이 User-Agent 문자열을 덮어쓰기로 결정한 경우는 예외다.”
조건이 붙은 보증입니다. 앱이 UA를 덮어쓰지 않은 경우에만 유효합니다.
// Android 웹뷰 판별. 앱이 UA를 덮어쓰지 않았을 때만 유효합니다
const ua = navigator.userAgent;
const isAndroid = /Android/.test(ua);
const isAndroidWebView = isAndroid && /\bwv\b/.test(ua);
여기서 UA 스니핑 라이브러리가 무너지는 첫 번째 지점이 나옵니다. Android 웹뷰의 UA에는 wv뿐 아니라 Chrome/버전 토큰도 그대로 들어 있습니다. Chrome 패턴만 매칭하는 라이브러리는 웹뷰를 Chrome으로 분류합니다. 웹뷰를 걸러내려면 Chrome 매칭에 wv 부재 조건을 직접 붙여야 합니다. 라이브러리가 알아서 해주기를 기대하면 안 됩니다.
무너진 것: 기기와 OS 정보
Android 17을 타깃(targetSdkVersion 37 이상)으로 빌드한 앱의 WebView부터 기본 UA 문자열이 축소됩니다. Android 17 기능 목록 페이지는 이 항목을 “Change (apps targeting 17+)“로 분류합니다. 조건은 기기의 OS 버전이 아니라 앱의 타깃 SDK입니다. 축소된 형태의 예시는 Android 개발자 블로그에 있습니다.
Mozilla/5.0 (Linux; Android 10; K; wv) AppleWebKit/537.36 (KHTML, like Gecko)
Version/4.0 Chrome/125.0.0.0 Mobile Safari/537.36
Linux; Android 10; K는 고정 문자열입니다. 실제 OS 버전도, 실제 기기 모델명도 아닙니다. 마이너·빌드·패치 버전도 0.0.0으로 뭉개집니다. 한 가지 함정이 있습니다. Google 블로그의 예시 문자열은 이 자리를 Chrome/125.000으로 적어두었는데, 같은 글의 본문 설명과 Chromium 문서는 모두 0.0.0이라고 적습니다. 블로그 예시 쪽이 오타이니 그대로 정규식에 옮기지 마십시오.
Android 17은 2026년 6월에 정식 배포되었습니다. 다만 기기가 17로 올라갔다고 축소된 UA가 바로 들어오지는 않습니다. 앱이 targetSdk를 37로 올리는 시점에 로그에 들어오기 시작합니다. 앱 팀에 targetSdk 상향 계획을 미리 확인해 두십시오.
남는 것과 사라지는 것을 정리하면 이렇습니다.
| 항목 | 축소 후 |
|---|---|
wv 토큰 | 남습니다 |
Chrome 메이저 버전 (Chrome/125) | 남습니다 |
| 기기 모델명 | 고정 문자열 K로 대체됩니다 |
| Android 릴리즈 번호 | 고정 문자열 Android 10으로 대체됩니다 |
| 마이너·빌드·패치 버전 | 0.0.0으로 대체됩니다 |
UA로 기기를 분기하던 코드는 이 시점에 조용히 무너집니다. 예외가 발생하지는 않고, 그냥 모든 기기가 같은 분기로 들어갑니다. 지금 코드베이스에 navigator.userAgent에서 모델명을 뽑는 로직이 있다면 그것부터 걷어내는 편이 낫습니다.
한 가지 관찰 사실은 남겨둘 만합니다. 축소된 UA에도 Chrome/125.0.0.0의 앞자리 같은 메이저 버전은 남습니다. Android WebView는 Chromium 기반이므로 이 숫자가 웹뷰 계열의 대략적 세대를 알려줍니다. 다만 이 숫자로 WebView 버전을 특정하라는 공식 권고는 없습니다. 참고 지표로만 쓰고, 판단 근거로는 뒤에 나올 WebView DevTools를 쓰십시오.
언제든 통째로 갈릴 수 있는 것: UA 전체
두 플랫폼 다 앱이 UA를 통째로 바꿀 수 있는 공식 API를 제공합니다. Android는 WebSettings.setUserAgentString(String)(API 레벨 3), iOS는 WKWebView.customUserAgent와 WKWebViewConfiguration.applicationNameForUserAgent(둘 다 iOS 9.0 이상)입니다.
앱이 이걸 쓰면 어떤 UA 스니핑 라이브러리도 맞출 수 없습니다. 게다가 부작용도 문서화되어 있습니다. Android 문서는 “이 방식으로 user-agent를 덮어쓰면 이 WebView의 User-Agent Client Hints 헤더와 navigator.userAgentData 값도 바뀔 수 있다”고 명시합니다. 같은 문서에 “KITKAT 버전부터는 페이지 로딩 중에 user-agent를 바꾸면 WebView가 로딩을 다시 시작한다”는 문장도 있습니다.
한 가지 세부가 실무에서 걸립니다. Android 블로그에 따르면 setUserAgentString()으로 커스텀 UA를 설정한 앱은 축소 대상에서 빠집니다. 그러나 getUserAgentString() 결과 뒤에 접미사만 붙이는 방식이라면 앞부분은 그대로 축소되고 붙인 접미사만 유지됩니다. UA에 자체 앱 식별 토큰을 심어 온 앱에서 흔한 패턴이 바로 이 접미사 방식입니다. “우리는 UA를 커스텀하니까 축소와 무관하다”는 앱 팀의 답변을 그대로 믿지 말고, 실제 문자열을 받아서 확인하십시오.
Client Hints는 대안이 되지 않습니다
Android WebView는 WebView 버전 116부터 User-Agent Client Hints를 지원합니다. 단 기본 UA 문자열을 보내는 앱에 한해서입니다. 그리고 navigator.userAgentData는 MDN 기준 Baseline이 아닙니다. MDN의 표현 그대로 “가장 널리 쓰이는 브라우저 일부에서 동작하지 않기 때문에” 제한적 가용성으로 분류되어 있고, 보안 컨텍스트(HTTPS)를 요구합니다.
iOS 웹뷰는 WebKit 엔진을 쓰므로 이 API에 기대지 않는 편이 안전합니다. 결론적으로 양 플랫폼 공통 진단 신호로는 쓸 수 없습니다.
그래서 어떡할까요
MDN은 UA 스니핑을 명시적으로 권하지 않습니다. “UA 문자열을 파싱해서 사이트 동작을 바꾸고 싶은 유혹이 있지만, 이것을 신뢰성 있게 하기는 매우 어렵고 종종 버그의 원인이 된다.” 이유도 함께 적혀 있습니다. “브라우저와 사용자 에이전트는 일상적으로 다른 브라우저인 척하거나, 여러 브라우저에 기반한 정보를 포함한다.”
권장 대안은 기능 탐지입니다. 어떤 브라우저인지 알아내는 대신, 필요한 기능이 있는지 직접 확인하고 없으면 대체 경로로 갑니다.
// 분기의 기준은 "어떤 브라우저인가"가 아니라 "이 기능이 있는가"입니다
if ('share' in navigator) {
await navigator.share({ url: location.href });
} else {
await copyToClipboard(location.href);
}
// CSS도 같은 방식으로 확인합니다
if (CSS.supports('height', '100dvh')) {
// 동적 뷰포트 단위를 씁니다
}
정리하면 UA의 용도는 두 가지로 좁아집니다. 첫째, wv 토큰으로 Android 웹뷰인지 아닌지 판별하는 것. 둘째, 앱이 심어준 자체 토큰으로 어느 앱의 웹뷰인지 식별하는 것. 그 밖의 분기는 기능 탐지로 옮기십시오.
두 번째 용도는 앱에 요청해야 하는 항목입니다. 뒤의 요청 목록에서 다시 다룹니다.
기기에서 웹뷰 버전을 직접 확인합니다
Android 쪽에는 앱 개발자에게 아무것도 요청하지 않고 웹 개발자가 혼자 확인할 수 있는 공식 도구가 있습니다. WebView DevTools입니다. 이 편에서 가장 실용적인 항목입니다.
Android 공식 문서 기준 실행 방법은 두 가지입니다.
Android 16 이상 + 개발자 모드라면 설정 앱에서 바로 들어갑니다.
설정 > 시스템 > 개발자 옵션 > WebView DevTools
그 밖의 최신 Android에서는 adb 한 줄입니다.
adb shell am start -a "com.android.webview.SHOW_DEV_UI"
앱은 네 영역으로 구성되어 있습니다. 공식 문서의 설명 그대로입니다.
| 영역 | 용도 |
|---|---|
| Home | 버전 정보 확인, 기본 WebView를 사전 배포 채널로 전환 |
| Crashes | WebView 크래시 리포트 목록과 업로드 |
| Flags | WebView 동작을 바꾸는 개발자 플래그 설정 |
| Net Logs | WebView의 저수준 네트워킹 로그 목록과 공유 |
여기서 Home 화면의 버전 정보 하나만 얻어도 제보의 성격이 완전히 달라집니다. “갤럭시에서 안 돼요”가 “WebView 128 채널 Stable에서 재현됩니다”가 됩니다. QA에 제보 양식을 만들 때 이 한 줄을 필수 항목으로 넣으면 재현 시간이 크게 줄어듭니다.
앱 쪽에서 코드로 읽을 수도 있습니다. WebView.getCurrentWebViewPackage()가 현재 로드된 WebView 제공 패키지의 PackageInfo를 돌려줍니다. API 레벨 26에 추가되었고, AndroidX 대안인 WebViewCompat.getCurrentWebViewPackage(Context)는 androidx.webkit 1.1.0부터 있습니다. 다만 기기 설정이 잘못되었거나, Wear OS처럼 WebView를 지원하지 않거나, 갱신 가능한 WebView 구현이 없는 기기에서는 null을 반환합니다.
문서에 붙은 주의도 함께 봐두면 좋습니다. WebView 패키지는 갱신·비활성화·제거되거나 개발자 설정으로 바뀔 수 있고, 바뀌면 WebView를 로드한 앱 프로세스가 종료됩니다. 웹뷰를 띄운 채로 앱이 갑자기 죽었다는 제보가 있다면 이 경우일 수 있습니다.
iOS에는 이에 대응하는 도구가 없습니다. 필요하지도 않습니다. 엔진 버전이 OS 버전에 묶여 있으니 설정 앱에서 iOS 버전만 확인하면 그것이 곧 엔진 버전입니다.
실기기에 디버거를 붙입니다
버전을 특정했으면 다음은 콘솔입니다. 실제 화면 안에서 DOM을 보고 스크립트를 실행할 수 있어야 진단이라고 할 수 있습니다.
iOS: 세 곳을 켜야 합니다
Mac의 Safari, 기기의 설정, 그리고 앱의 코드. 세 관문이 전부 열려야 붙습니다.
첫째, Mac의 Safari에서 개발자용 메뉴를 켭니다. Apple 지원 문서의 안내 그대로입니다. 메뉴 막대에 Develop 메뉴가 보이지 않으면 Safari 설정의 고급 탭에서 “웹 개발자용 기능 보기”를 선택합니다.
둘째, iOS 기기에서 웹 인스펙터(Web Inspector)를 켭니다. Apple 개발자 문서의 현행 경로는 이렇습니다.
설정 앱 > Apps > Safari > Advanced > Web Inspector 토글
iOS 18에서 설정 앱이 개편되며 상위에 Apps 항목이 생겼습니다. 그 이전 버전에서는 설정 앱 최상위에서 바로 Safari로 들어갑니다. 켠 다음 케이블로 Mac에 연결하면 Safari의 Develop 메뉴에 기기가 나타납니다. 기기가 신뢰 여부를 물으면 승인해야 목록에 뜹니다. 케이블로 한 번 붙이고 나면 기기 하위 메뉴에서 네트워크 연결을 활성화할 수 있습니다. 시뮬레이터는 웹 인스펙터가 항상 켜져 있어 이 과정이 필요 없습니다.
셋째, 앱이 그 웹뷰를 검사 가능으로 표시해야 합니다. 여기가 실무에서 가장 자주 막히는 지점입니다.
iOS 16.4 / macOS 13.3부터 WKWebView에 isInspectable 프로퍼티가 생겼습니다(Objective-C에서는 inspectable). Apple 문서의 문장이 명확합니다. “기본값은 false다. 뷰의 생애 중 어느 시점에든 true로 설정하면 Safari 웹 인스펙터가 그 뷰의 콘텐츠를 검사할 수 있게 된다.”
앱 개발자에게 요청할 지점은 딱 이 한 줄입니다.
// 앱에 이 한 줄을 켜달라고 요청하는 지점입니다. 그 이상은 앱 팀의 영역입니다
if #available(macOS 13.3, iOS 16.4, tvOS 16.4, *) {
webView.isInspectable = true
}
여기서 흔한 오해를 하나 정리해야 합니다. “iOS 16.4 미만에서는 웹뷰 디버깅이 안 된다”는 틀린 서술입니다. 사실은 거의 반대입니다. WebKit 블로그의 설명은 이렇습니다. macOS 13.3 / iOS 16.4 이전 SDK로 링크된 앱의 WKWebView는 예전 동작을 유지합니다. 즉 Xcode에서 디버그용으로 빌드했을 때는 항상 검사 가능했습니다. 대신 릴리즈 앱은 전혀 열 수 없었습니다.
16.4는 그 이분법을 깬 API입니다. 앱이 원하면 릴리즈 빌드에서도 열 수 있게 만든 것입니다. 그 대가가 하나 붙었습니다. 같은 블로그의 문장입니다. “최신 SDK로 링크하면서 구버전 macOS와 iOS를 지원하는 앱은, 무엇이 검사 가능하고 무엇이 아닌지에 대한 혼란을 피하기 위해 디버그 빌드에서 모든 콘텐츠가 검사 가능해지던 이전 동작을 얻지 않는다.”
최신 SDK로 빌드한 앱은 디버그 빌드조차 저 한 줄이 없으면 열리지 않습니다. 앱 개발자가 “디버그 빌드니까 당연히 붙습니다”라고 답한다면, 그 전제가 SDK 버전에 따라 깨졌다는 사실을 알려주면 됩니다.
이 시리즈의 관통 명제에 가장 잘 맞는 사례가 이것입니다. 안 되는 게 아니라, 안 켠 것입니다.
붙고 나면 Safari의 Develop 메뉴에서 연결된 기기 하위 메뉴에 검사 가능한 콘텐츠가 나타납니다. Apple 문서는 Safari가 웹페이지와 JavaScript 컨텍스트를 앱별로 분리해서 보여준다고 설명합니다. 앱이 여러 웹뷰를 띄워도 찾기 쉽습니다.
Android: 앱이 한 번 호출해야 합니다
Android는 관문이 둘입니다. 기기의 USB 디버깅과 앱의 명시적 호출입니다.
앱 쪽 요청 항목은 이 한 줄입니다.
// 앱에 이 호출을 넣어달라고 요청하는 지점입니다
// API 19 가드는 오늘날 형식적입니다. WebView 업데이트 자체가 Android 10 이상만 지원합니다
WebView.setWebContentsDebuggingEnabled(true)
Android 레퍼런스의 설명은 “이 애플리케이션의 모든 WebView에 로드된 웹 콘텐츠(HTML/CSS/JavaScript)의 디버깅을 활성화한다”입니다. API 레벨 19에 추가된 정적 메서드이고, 개별 웹뷰가 아니라 앱 전체에 일괄 적용됩니다.
기본값에 관해서는 두 문서가 서로 다른 이야기를 하는 것처럼 보이는데, 실은 모순이 아닙니다.
- Android 레퍼런스: “WebView 113.0.5656.0 이상에서는 앱이 매니페스트에
android:debuggable="true"로 선언되어 있으면 자동으로 활성화된다. 그 외에는 기본값이false다.” - Chrome DevTools 문서: “WebView 디버깅은 애플리케이션 매니페스트의
debuggable플래그 상태에 영향을 받지 않는다.debuggable이 true일 때만 WebView 디버깅을 켜고 싶다면 런타임에 플래그를 확인하라.”
두 문장을 합치면 이렇게 정리됩니다. debuggable이 false여도 앱이 명시 호출하면 켜지고, debuggable이 true면 WebView 113 이상에서 호출 없이도 켜집니다. 참고로 Chrome DevTools의 WebView 전용 페이지는 최종 갱신이 2015년으로 표기되어 있어 113 이상의 자동 활성화 동작이 반영되어 있지 않습니다. 두 문서를 함께 보셔야 합니다.
결론은 같습니다. 스토어에 올라간 릴리즈 빌드는 양쪽 다 자동으로 열리지 않습니다.
기기 쪽 절차는 Chrome DevTools 문서의 순서 그대로입니다.
1. Android 기기에서 개발자 옵션 화면을 엽니다
2. USB 디버깅을 활성화합니다
3. 데스크톱 Chrome에서 chrome://inspect#devices 로 이동합니다
4. 처음 연결하는 기기라면 Offline 상태로 뜹니다.
기기 화면에 나타나는 디버깅 세션 승인 창을 수락합니다
5. 디버깅하려는 웹뷰 아래의 inspect 를 누릅니다
목록에 안 보일 때의 확인 순서도 문서에 있습니다. 앱에서 WebView 디버깅이 켜져 있는지 확인하고, 기기에서 해당 웹뷰가 있는 화면을 실제로 연 다음, chrome://inspect 페이지를 새로고침합니다. 페이지를 먼저 열어두고 앱을 나중에 켜면 목록이 갱신되지 않는 경우가 있습니다.
두 플랫폼 절차 비교
| 관문 | iOS | Android |
|---|---|---|
| 데스크톱 | Safari에서 개발자용 기능 활성화 | Chrome에서 chrome://inspect |
| 기기 | 설정 > Apps > Safari > Advanced > Web Inspector | 개발자 옵션 > USB 디버깅 |
| 앱 | webView.isInspectable = true (16.4 이상) | WebView.setWebContentsDebuggingEnabled(true) |
| 앱 요청 없이 되는 경우 | 16.4 이전 SDK로 링크한 앱의 디버그 빌드 | WebView 113 이상 + android:debuggable="true" |
릴리즈 빌드에서는 대체로 닫혀 있습니다
여기부터는 실무의 현실입니다. 위 절차는 전부 개발용 빌드가 손에 있을 때 성립합니다. 운영 앱에서만 재현되는 증상이라면 인스펙터가 붙지 않습니다.
“그럼 릴리즈 빌드에서도 켜달라고 요청하면 되지 않나”라고 생각하기 쉽지만, 그 요청은 가볍지 않습니다. Android 공식 문서의 경고를 그대로 옮깁니다.
웹 콘텐츠 디버깅을 활성화하면 앱의 어떤 WebView든 그 상태를 사용자가 adb를 통해 검사하고 수정할 수 있게 됩니다. 이것은 보안 부채이며, 앱의 명시적 의도가 아닌 한 앱의 프로덕션 빌드에서 활성화해서는 안 됩니다.
보통 이 요청이 반려되는 것은 정당합니다. 그러니 요청하기 전에 대안부터 준비하십시오. 요청이 통과되더라도 심사와 배포를 기다려야 하고, 그동안 진단은 멈춰 있습니다.
웹 쪽에서 스스로 잡습니다
콘솔을 못 봐도 웹은 자기 에러를 잡을 수 있습니다. 표준 이벤트가 있습니다.
// 동기적으로 던져진 스크립트 에러
window.addEventListener('error', (event) => {
report({
kind: 'error',
message: event.message,
source: event.filename,
line: event.lineno,
column: event.colno,
stack: event.error?.stack,
});
});
// 처리되지 않은 Promise 거부는 별개의 이벤트입니다
window.addEventListener('unhandledrejection', (event) => {
report({ kind: 'rejection', reason: String(event.reason) });
});
두 이벤트를 반드시 함께 걸어야 합니다. MDN의 설명대로 error 이벤트는 동기적으로 던져진 스크립트 에러만 잡습니다. Promise가 거부되었고(async 함수 안의 잡히지 않은 throw 포함) 거부 핸들러가 붙어 있지 않으면 unhandledrejection이 발생합니다. 요즘 코드에서 실제 사고의 상당수는 후자입니다.
여기에 환경 스냅샷을 함께 보내면 제보의 질이 달라집니다.
function envSnapshot() {
const ua = navigator.userAgent;
return {
ua,
isAndroidWebView: /Android/.test(ua) && /\bwv\b/.test(ua),
viewport: `${window.innerWidth}x${window.innerHeight}`,
dpr: window.devicePixelRatio,
lang: navigator.language,
// 배포 식별자. 앱 캐시 때문에 구버전이 도는지 여기서 드러납니다
build: __BUILD_ID__,
};
}
마지막 항목이 중요합니다. 앱에서 배포 전 화면이 보이는 문제는 이 값 하나로 판단할 수 있습니다. 그 계통은 ep.08에서 따로 다룹니다.
전송 실패에도 대비해야 합니다. 화면이 곧 닫히거나 앱이 백그라운드로 내려가면 일반 요청은 잘립니다.
function report(payload) {
const body = JSON.stringify({ ...payload, env: envSnapshot() });
// 페이지가 사라지는 중에도 전송이 유지됩니다
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/client-log', body);
return;
}
fetch('/api/client-log', { method: 'POST', body, keepalive: true });
}
화면에 직접 찍는 최후 수단
원격 로그 인프라가 없거나, 있어도 QA가 그 대시보드를 못 보는 상황이 있습니다. 그럴 때는 화면에 찍는 것이 가장 빠릅니다.
// 특정 쿼리스트링이 붙었을 때만 나타나는 진단 패널
// 운영에 올라가도 일반 사용자에게는 보이지 않습니다
if (new URLSearchParams(location.search).has('__diag')) {
const el = document.createElement('pre');
el.style.cssText =
'position:fixed;inset:auto 0 0 0;z-index:2147483647;' +
'margin:0;padding:8px;max-height:40vh;overflow:auto;' +
'font-size:11px;line-height:1.4;white-space:pre-wrap;' +
'background:#000;color:#0f0';
el.textContent = JSON.stringify(envSnapshot(), null, 2);
document.body.appendChild(el);
}
URL 변경이 가능하다면, QA에게 “URL 뒤에 ?__diag 붙이고 캡처해서 보내주세요”라고 하면 UA와 뷰포트와 빌드 번호가 한 장에 담겨 옵니다. 다만 이 패널은 진단 정보를 화면에 노출하므로 개인정보나 세션 값은 절대 담지 마십시오. 아래 PII 주의와 같은 원칙입니다.
네이티브 로그로 빼는 통로
웹 로그를 네이티브 로그와 같은 타임라인에 놓아야 풀리는 문제가 있습니다. 그럴 때 요청할 항목이 플랫폼별로 하나씩 있습니다.
Android는 WebChromeClient.onConsoleMessage(ConsoleMessage)입니다. 웹 페이지의 console 출력을 logcat으로 뺍니다. Android 문서는 콘솔 메시지가 Logcat에 나타나려면 onConsoleMessage()를 구현한 WebChromeClient를 제공하고 setWebChromeClient()로 웹뷰에 붙여야 한다고 명시합니다. ConsoleMessage에는 messageLevel()로 조회하는 심각도(DEBUG, TIP, LOG, WARNING, ERROR 다섯 단계)가 담깁니다. 분기를 넷만 두면 console.debug() 출력이 갈 곳을 잃습니다. 이 페이지는 setWebContentsDebuggingEnabled나 디버그 빌드를 전제로 요구하지 않습니다.
로그를 찍을 때 항상 중요한 건 “로그 메시지에 개인 식별 정보(PII)를 포함하지 마십시오.” 민감한 식별자를 다루는 서비스에서는 이 문장이 특히 무겁습니다. logcat은 기기에 남고, 같은 기기의 다른 경로로 읽힐 수 있습니다. 개인 식별 정보, 인증 토큰, 세션 값, 응답 본문을 console.log에 그대로 흘리는 코드가 남아 있다면 이 통로를 여는 순간 그것이 전부 로그로 나갑니다. 통로를 열기 전에 로그 문장을 먼저 감사하십시오.
iOS는 WKScriptMessageHandler입니다. 정확히 말하면 이것은 로깅 API가 아니라 웹에서 네이티브로 메시지를 보내는 공식 통로입니다. WKUserContentController.add(_:name:)으로 핸들러를 설치하고, 웹에서는 이렇게 호출합니다.
// 앱이 'log'라는 이름으로 메시지 핸들러를 설치해 둔 경우에만 동작합니다
// 없으면 조용히 무시되도록 옵셔널 체이닝으로 감쌉니다
window.webkit?.messageHandlers?.log?.postMessage({
level: 'error',
message: 'checkout submit failed',
env: envSnapshot(),
});
Apple 문서는 JavaScript에서 window.webkit.messageHandlers.핸들러이름.postMessage(메시지본문) 형태로 호출한다고 설명하고, 핸들러 이름은 WKUserContentController에 설치할 때 지정한 값이라고 명시합니다. JavaScript로 응답을 돌려주고 싶으면 WKScriptMessageHandlerWithReply 프로토콜로 구현하라고 안내합니다.
이것을 로그 통로로 쓰는 것은 응용입니다. Apple이 로깅 용도로 문서화한 것은 아닙니다. 그리고 이 브릿지는 진단을 넘어 앱과 웹이 주고받는 모든 것의 통로가 되므로, 설계 관점의 논의는 ep.09에서 다시 다룹니다.
그래서 무엇을 하면 되나
두 갈래로 갈라둡니다. 앞의 것은 오늘 혼자 할 수 있고, 뒤의 것은 요청해야 합니다.
웹 개발자가 할 일
하나. UA에서 기기 분기 로직을 걷어냅니다.
Android 17의 UA 축소는 앱이 targetSdkVersion 37 이상으로 올라가는 시점에 적용되고, 그때 기기 모델명과 OS 릴리즈 번호는 고정 문자열로 대체됩니다. 그 분기는 예외 없이 조용히 무너집니다. 기능 탐지(CSS.supports, in 연산자)로 옮기십시오.
둘. wv 토큰 판별만 남깁니다.
Android 웹뷰인지 아닌지는 이 토큰으로 판별할 수 있고 축소 후에도 남습니다. 다만 앱이 UA를 덮어쓰지 않았을 때만 유효하다는 조건을 주석으로 남겨두십시오.
셋. 전역 에러 핸들러와 환경 스냅샷을 붙입니다.
error와 unhandledrejection 둘 다 겁니다. 스냅샷에 배포 식별자를 반드시 넣습니다. 릴리즈 빌드에 인스펙터가 안 붙는 상황이 기본값이라고 전제하고 설계하십시오.
넷. 쿼리스트링으로 열리는 진단 패널을 만들어 둡니다.
QA와 고객센터가 캡처 한 장으로 환경을 보고할 수 있게 됩니다. 개인정보와 세션 값은 담지 않습니다.
다섯. console.log 문장을 감사합니다.
네이티브 로그 통로를 요청할 계획이라면 그전에 해야 합니다. PII가 흘러가는 경로를 스스로 만들면 안 됩니다.
여섯. 제보 양식에 세 항목을 필수로 넣습니다.
OS 버전, Android라면 WebView DevTools에서 읽은 WebView 버전, 그리고 앱 버전. 이 세 줄이면 “특정 기기에서만”이 사라집니다.
앱에 요청할 일
요청은 네 개입니다. 그 이상 네이티브로 들어갈 필요가 없습니다.
| 요청 | 플랫폼 | 이유 |
|---|---|---|
webView.isInspectable = true | iOS 16.4 이상 | 이게 없으면 최신 SDK 빌드는 디버그에서도 인스펙터가 안 붙습니다 |
WebView.setWebContentsDebuggingEnabled(true) | Android | 앱 전체 웹뷰에 일괄 적용됩니다 |
| UA에 앱 식별 토큰 추가 | iOS applicationNameForUserAgent, Android setUserAgentString | 서버 로그에서 앱 웹뷰 트래픽을 분리할 수 있습니다 |
WebChromeClient.onConsoleMessage 구현 | Android | 웹 로그를 logcat 타임라인에 합칩니다 |
세 번째 항목은 조건을 붙여 요청하십시오. 접미사 방식으로 붙이면 앞부분은 Android 17 축소 대상 그대로입니다. 그래도 앱 식별 목적에는 충분하고, 오히려 그편이 표준 신호를 보존하므로 낫습니다. 요청할 때 원하는 최종 문자열 형태를 함께 적어주면 왕복이 줄어듭니다.
첫 두 항목은 빌드 종류를 명시해서 요청해야 합니다. “개발·스테이징 빌드에서만 켜주세요”가 기본이고, 운영 빌드는 별도 판단입니다. Android 공식 문서가 프로덕션 활성화를 보안 부채로 명시하고 있으니, 그 문장을 근거로 함께 제시하면 앱 팀과의 합의가 빨라집니다. 요청 문서를 어떻게 쓰는지는 ep.09에서 양식까지 정리합니다.
여기까지 하면 “브라우저에서는 되는데요”라는 문장 다음에 놓을 문장이 생깁니다. 어느 OS, 어느 웹뷰 버전, 어느 앱 빌드에서 재현되는지가 특정됩니다. 그다음부터가 실제 증상의 영역입니다.
참고 자료
- Apple. WKWebView. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebview
- Apple. isInspectable. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebview/isinspectable
- Apple. customUserAgent. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebview/customuseragent
- Apple. applicationNameForUserAgent. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/applicationnameforuseragent
- Apple. WKScriptMessageHandler. Apple Developer Documentation. https://developer.apple.com/documentation/webkit/wkscriptmessagehandler
- Apple. UIWebView. Apple Developer Documentation. https://developer.apple.com/documentation/uikit/uiwebview
- Apple. SFSafariViewController. Apple Developer Documentation. https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller
- Apple. ASWebAuthenticationSession. Apple Developer Documentation. https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession
- Apple. Inspecting iOS and iPadOS. Apple Developer Documentation. https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios
- Apple. Safari Release Notes. Apple Developer Documentation. https://developer.apple.com/documentation/safari-release-notes
- Apple. Safari 26.6 Release Notes. Apple Developer Documentation. https://developer.apple.com/documentation/safari-release-notes/safari-26\_6-release-notes
- Apple. Deadline extended for app updates using UIWebView. Apple Developer News. https://developer.apple.com/news/?id=edwud51q
- Apple. App Review Guidelines. Apple Developer. https://developer.apple.com/app-store/review/guidelines/
- Apple. Alternative browser engines in the EU. Apple Developer. https://developer.apple.com/support/alternative-browser-engines/
- Apple. Use the developer tools in the Develop menu in Safari on Mac. Apple Support. https://support.apple.com/guide/safari/use-the-developer-tools-in-the-develop-menu-sfri20948/mac
- WebKit. Enabling the Inspection of Web Content in Apps. WebKit Blog. https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/
- Google. WebView. Android Developers. https://developer.android.com/reference/android/webkit/WebView
- Google. WebSettings. Android Developers. https://developer.android.com/reference/android/webkit/WebSettings
- Google. WebViewCompat. Android Developers. https://developer.android.com/reference/androidx/webkit/WebViewCompat
- Google. ConsoleMessage.MessageLevel. Android Developers. https://developer.android.com/reference/android/webkit/ConsoleMessage.MessageLevel
- Google. Manage WebView objects. Android Developers. https://developer.android.com/develop/ui/views/layout/webapps/managing-webview
- Google. Debug using WebView DevTools App. Android Developers. https://developer.android.com/develop/ui/views/layout/webapps/debug-webview-devtools-app
- Google. Debug using JavaScript console logs. Android Developers. https://developer.android.com/develop/ui/views/layout/webapps/debug-javascript-console-logs
- Google. Android 4.4 APIs. Android Developers. https://developer.android.com/about/versions/kitkat
- Google. Android 17 features and changes list. Android Developers. https://developer.android.com/about/versions/17/summary
- Google. User-Agent Reduction on Android WebView. Android Developers Blog. https://android-developers.googleblog.com/2024/12/user-agent-reduction-on-android-webview.html
- Google. Remote debug WebViews. Chrome DevTools. https://developer.chrome.com/docs/devtools/remote-debugging/webviews
- Google. Remote debug Android devices. Chrome DevTools. https://developer.chrome.com/docs/devtools/remote-debugging
- The Chromium Projects. User-Agent Reduction. Chromium Updates. https://www.chromium.org/updates/ua-reduction/
- The Chromium Projects. Understanding WebView Channels. WebView docs. https://chromium.googlesource.com/chromium/src/+/HEAD/android\_webview/docs/channels.md
- The Chromium Projects. Legacy OS versions. WebView docs. https://chromium.googlesource.com/chromium/src/+/HEAD/android\_webview/docs/legacy-os-behavior.md
- The Chromium Projects. WebView for AOSP system integrators. WebView docs. https://chromium.googlesource.com/chromium/src/+/HEAD/android\_webview/docs/aosp-system-integration.md
- The Chromium Projects. WebView Providers. WebView docs. https://chromium.googlesource.com/chromium/src/+/refs/heads/main/android\_webview/docs/webview-providers.md
- MDN. Browser detection using the user agent. MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Browser\_detection\_using\_the\_user\_agent
- MDN. Navigator: userAgentData property. MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
- MDN. Window: error event. MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/API/Window/error\_event
다음 편 예고
진단 도구를 붙였으니 이제 증상으로 들어갑니다. 새 창, 외부 앱 호출, 다운로드, 인쇄는 전부 웹뷰의 같은 결정 지점을 통과합니다. 그 지점이 비어 있으면 넷 다 조용히 사라집니다.