라벨이 WebView인 게시물 표시

안드로이드 웹뷰에서 파일 업로드(input type="file") 구현 및 콜백 오류 해결 방법

하이브리드 앱을 운영하다 보면 웹 페이지의 <input type="file"> 버튼을 눌러도 아무 반응이 없다는 고객 피드백을 마주할 때가 있습니다. 안드로이드 웹뷰는 기본적으로 파일 선택 요청만 던질 뿐, 실제 파일 탐색기를 띄우고 결과를 받아오는 UI 처리는 앱(Native)이 직접 중개해야 하기 때문입니다. 저의 경우도 최근 작업 중에 "파일 선택을 한 번 취소하고 나면, 그 다음부터 버튼이 아예 먹통이 되는 현상"을 겪었습니다. 원인을 파악해 보니 웹뷰의 파일 선택 콜백( ValueCallback ) 생명주기 관리 문제였습니다. 나중에 또 같은 문제로 헤매지 않기 위해, 실제 서비스 적용이 가능한 구현 코드와 운영 시 꼭 챙겨야 할 주의점을 정리해 둡니다. 내가 겪은 문제와 원인 웹 페이지가 파일 선택을 요청하면 앱은 WebChromeClient.onShowFileChooser() 에서 이 요청을 받게 됩니다. 이때 웹뷰는 결과를 기다리는 대기 상태로 들어가며, 앱은 ValueCallback<Uri[]> 객체에 사용자가 선택한 파일 경로를 담아 웹뷰에 돌려줘야 합니다. 여기서 가장 자주 하는 실수가 "사용자가 파일 선택을 취소했을 때 아무런 처리를 하지 않는 것"입니다. 콜백에 결과를 돌려주지 않고 그냥 끝나버리면, 웹뷰는 여전히 이전 요청을 대기하는 상태로 남아있게 되어 다음 번 클릭부터는 아예 반응을 하지 않게 됩니다. 해결 코드 (바로 적용하는 구현 예시) 최근 안드로이드 개발 표준에 맞춰 기존의 startActivityForResult 대신 Activity Result API ( registerForActivityResult )를 사용해 구현한 코드입니다. 보안을 위해 실제 업로드 URL이나 로컬 파일 경로는 제외하고 틀만 잡았습니다. Kotlin class UploadWebViewActivity : AppCompatActivity () { private ...

Android WebView 파일 다운로드 구현 (DownloadManager vs SAF 선택 기준과 보안 설정)

WebView 기반으로 앱을 빌드하고 운영하다 보면, 웹 페이지 내의 파일 다운로드 기능을 처리해야 할 때가 꼭 생깁니다. 처음에는 단순히 다운로드 링크를 누르면 알아서 받아지겠지 생각하기 쉽지만, 실제로 운영해보면 그렇지가 않습니다. 다운로드 URL 검증부터 파일명 정제, Android 버전별 저장소 정책, 보안 검수까지 사전에 챙겨야 할 항목들이 꽤 많습니다. 저의 경우도 예전에 대충 넘겼다가 구글 플레이 보안 관련 지적을 받거나, Android 버전이 올라가면서 저장소 권한 문제로 다운로드가 안 된다는 사용자 피드백을 받고 식은땀을 흘린 적이 있습니다. 자꾸 까먹기도 해서, 실무에서 바로 쓸 수 있도록 다운로드 구현 기준과 필수 체크리스트를 정리해 둡니다. 1. WebView 파일 다운로드 시 자주 겪는 문제들 보통 다운로드 기능을 대충 구현하면 아래와 같은 지점에서 문제가 터집니다. URL 검증 미흡: 다운로드 URL을 검증하지 않고 그대로 DownloadManager 에 넘기면, 알 수 없는 스킴이나 피싱 목적의 HTTP URL까지 처리하게 되어 보안상 위험합니다. 파일명 오염: 서버가 내보낸 Content-Disposition 값을 그대로 믿고 쓰면, 파일 이름에 허용되지 않는 특수문자가 섞여 있거나 이름이 너무 길어서 저장 실패가 날 수 있습니다. MIME Type 누락: 파일 타입을 제대로 지정하지 않으면 사용자가 다운로드 폴더에서 파일을 열 때 어떤 앱으로 열어야 하는지 기기가 인지하지 못합니다. 저장소 권한 반려: Android 10(API 29) 이후 Scoped Storage 정책을 고려하지 않고 과거 방식대로 공용 저장소에 직접 쓰려고 하면 앱이 뻗거나 구글 검수에서 권한 과다 요청으로 반려됩니다. 인증 토큰 노출: 다운로드 URL의 Query 스트링에 Access Token이나 개인 식별자를 그대로 태워 보내면, 시스템 로그나 알림 창, 브라우저 기록에 고스란히 남는 보안 취약점이 발생합니다. 운영 관점에서 파일 다운로드는 ...

Android WebView에서 이미지나 스크립트가 안 나올 때 (Mixed Content, Cleartext Traffic 오류 해결)

Android WebView로 하이브리드 앱을 만들거나 기존 웹 서비스를 앱으로 감싸서 운영하다 보면 꼭 한 번씩 겪는 문제가 있습니다. "PC나 모바일 브라우저에서는 잘 열리는데, 앱 WebView 안에서만 이미지나 스크립트가 안 보입니다." 하는 상황입니다. 저의 경우도 예전에 운영 서버와 테스트 서버를 오가며 세팅하다가 이 문제로 한참 헤맨 적이 있어서, 나중에도 참고하려고 깔끔하게 정리해 둡니다. 단순 화면 깨짐으로 넘기기에는 보안상 중요한 부분이라 제대로 짚고 넘어가야 합니다. 원인 및 차이점 간단 정리 원인은 크게 Mixed Content(혼합 콘텐츠)와 Cleartext Traffic(일반 텍스트 트래픽) 두 가지로 나뉩니다. 둘 다 HTTP 통신 보안 정책 때문에 발생하는 문제인데, 확인해야 하는 위치가 다릅니다. Mixed Content: 메인 페이지는 https:// 로 안전하게 열렸는데, 그 안에서 불러오는 이미지, JS, CSS, 동영상 등의 주소가 http:// 로 되어 있는 경우입니다. WebView가 보안상 이 리소스 로드를 차단해 버립니다. ( WebView 설정 영역 ) Cleartext Traffic: 앱이 암호화되지 않은 생짜 http:// 통신 자체를 시도하는 것을 말합니다. Android 9 (API 28) 이상부터는 기본적으로 이 통신이 전부 차단됩니다. ( Network Security Config, Manifest 영역 ) 가장 좋은 해결책은 당연히 모든 리소스를 HTTPS로 바꾸는 것입니다. 하지만 개발 중이거나 부득이한 예외 상황이 있다면 아래처럼 안전하게 도메인을 제한해서 풀어야 합니다. 안전한 WebView 설정 (Kotlin 코드) 화면이 깨진다고 해서 운영 앱의 WebView 설정을 MIXED_CONTENT_ALWAYS_ALLOW 로 막 열어버리면 절대 안 됩니다. 중간에서 데이터가 변조될 위험이 크고 구글 플레이 심사에서도 거절 사유가 될 수 있습니다. 운영 앱에서는 MIXED_C...

Android WebView 안전하게 설정하는 방법 (JS 허용, 외부 URL 분기, 파일 접근 차단)

하이브리드 앱을 개발하거나 운영하다 보면 앱 내부에 웹 화면을 넣기 위해 WebView를 정말 자주 쓰게 됩니다. 저의 경우도 공지사항, 이벤트 페이지, 또는 결제 화면 등을 WebView로 처리하곤 하는데요. 웹 페이지가 정상적으로 동작해야 하니까 처음 개발할 때 별생각 없이 JavaScript를 켜고 관련 설정을 느슨하게 다 열어두는 경우가 많습니다. 하지만 이렇게 설정을 열어두면 XSS 같은 웹 보안 취약점이 그대로 앱의 취약점으로 이어집니다. 특히 내부 도메인과 외부 링크 구분이 안 되거나, file:// 접근이 허용된 상태에서 JavaScript가 켜지면 로컬에 있는 민감한 데이터가 유출될 위험도 있습니다. 매번 프로젝트 세팅할 때마다 설정 값이 헷갈려서, 실제 운영 환경에 맞춰 안전하게 쓸 수 있는 가이드와 공통 코드를 정리해 둡니다. 1. 안전한 WebView 공통 설정 코드 매번 화면마다 설정을 따로 복사해서 넣으면 누락되는 부분이 생기기 쉽습니다. 아래처럼 공통으로 검증하고 세팅할 수 있는 Configurator 클래스를 만들어 두고 호출하는 방식이 안전합니다. Kotlin class SecureWebViewConfigurator ( private val allowedHosts: Set<String>, ) { fun configure (webView: WebView ) { webView.settings.apply { // 웹 서비스 동작을 위해 필요한 경우만 켭니다. javaScriptEnabled = true domStorageEnabled = true // 로컬 파일 접근은 기본적으로 모두 차단합니다. allowFileAccess = false allowContentAccess = false allowFileAccessFromFil...