Android WebChromeClient 완벽 가이드

✨ 개요

WebViewClient페이지 탐색 흐름과 로딩 수명주기를 다루는 핵심 콜백 집합입니다.
링크 열기 경로(내부/외부), 에러 화면, SSL 처리, 리디렉션, 리소스 가로채기까지 모두 WebViewClient 영역이죠.
이 글에서는 실무에서 바로 쓰는 기본 세팅 + 콜백별 베스트 프랙티스를 정리합니다. —

1 언제 WebViewClient를 쓰나?

WebChromeClient 완벽 가이드 – 파일 업로드, 진행률, JS 다이얼로그, 전체화면 영상까지 (Kotlin)

WebChromeClient는 WebView에서 브라우저 UI 성격의 기능을 처리하는 클라이언트입니다.
진행률(Progress), 페이지 제목/아이콘, 파일 업로드(input type=”file”), JS 다이얼로그(alert/confirm/prompt), 권한(Camera/Mic/Geolocation), 콘솔 로그, 전체화면 동영상 등을 담당합니다.

비교: 네비게이션/에러/SSL/리소스는 WebViewClient, 브라우저 UI는 WebChromeClient.


2 기본 세팅 템플릿

@SuppressLint("SetJavaScriptEnabled")
fun WebView.setupChrome(
    onProgress: (Int) -> Unit = {},
    onTitle: (String?) -> Unit = {},
    pickFiles: (mime: String, allowMultiple: Boolean, cb: (Array<Uri>) -> Unit) -> Unit,
    enterFullscreen: (view: View, cb: WebChromeClient.CustomViewCallback) -> Unit,
    exitFullscreen: () -> Unit
) = apply {
        settings.javaScriptEnabled = true
        settings.domStorageEnabled = true

        webChromeClient = object : WebChromeClient() {

            // 1) 진행률 0..100
            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                onProgress(newProgress)
            }

            // 2) 페이지 제목/아이콘
            override fun onReceivedTitle(view: WebView?, title: String?) {
                onTitle(title)
            }

            // 3) 파일 업로드 (input type="file")
            override fun onShowFileChooser(
                webView: WebView,
                filePathCallback: ValueCallback<Array<Uri>>,
                fileChooserParams: FileChooserParams
            ): Boolean {
                val accept = fileChooserParams.acceptTypes.firstOrNull()?.ifBlank { "*/*" } ?: "*/*"
                val multiple = fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE
                // 앱 쪽(Activity Result API) 파일 선택기로 위임
                pickFiles(accept, multiple) { uris ->
                    filePathCallback.onReceiveValue(uris)
                }
                return true
            }

            // 4) JS 다이얼로그 (alert/confirm/prompt)
            override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult): Boolean {
                val ctx = view?.context ?: return false
                AlertDialog.Builder(ctx)
                    .setMessage(message)
                    .setPositiveButton(android.R.string.ok) { _, _ -> result.confirm() }
                    .setOnCancelListener { result.cancel() }
                    .show()
                return true
            }
            override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult): Boolean {
                val ctx = view?.context ?: return false
                AlertDialog.Builder(ctx)
                    .setMessage(message)
                    .setPositiveButton(android.R.string.ok) { _, _ -> result.confirm() }
                    .setNegativeButton(android.R.string.cancel) { _, _ -> result.cancel() }
                    .show()
                return true
            }
            override fun onJsPrompt(
                view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult
            ): Boolean {
                val ctx = view?.context ?: return false
                val input = EditText(ctx).apply { setText(defaultValue ?: "") }
                AlertDialog.Builder(ctx)
                    .setMessage(message)
                    .setView(input)
                    .setPositiveButton(android.R.string.ok) { _, _ -> result.confirm(input.text.toString()) }
                    .setNegativeButton(android.R.string.cancel) { _, _ -> result.cancel() }
                    .show()
                return true
            }

            // 5) 콘솔 로그 (개발 편의)
            override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
                Log.d("WV-CONSOLE", "[${consoleMessage.messageLevel()}] ${consoleMessage.message()} @${consoleMessage.sourceId()}:${consoleMessage.lineNumber()}")
                return super.onConsoleMessage(consoleMessage)
            }

            // 6) 전체화면 영상(HTML5 video, <iframe> 등)
            override fun onShowCustomView(view: View, callback: CustomViewCallback) {
                enterFullscreen(view, callback)
            }
            override fun onHideCustomView() {
                exitFullscreen()
            }

            // 7) 카메라/마이크 권한 (WebRTC 등)
            override fun onPermissionRequest(request: PermissionRequest) {
                // 요청한 리소스
                val wants = request.resources // ex) VIDEO_CAPTURE, AUDIO_CAPTURE
                // 1) Android 런타임 권한 확인/요청 (CAMERA/RECORD_AUDIO)
                // 2) 승인 후에만 request.grant(wants)
                // 3) 거부 시 request.deny()
            }

            // 8) 위치 권한(지오로케이션)
            override fun onGeolocationPermissionsShowPrompt(origin: String?, callback: GeolocationPermissions.Callback) {
                // 1) ACCESS_FINE_LOCATION 런타임 권한 체크/요청
                // 2) 허용 시 callback.invoke(origin, true, false), 거부 시 false
            }
        }
    }

3 보안/권한 체크리스트

WebSettingsCompat.setSafeBrowsingEnabled(webView.settings, true)
webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW


4 생명주기 & 메모리 주의


5. 결론



Related Posts