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

하이브리드 앱을 운영하다 보면 웹 페이지의 <input type="file"> 버튼을 눌러도 아무 반응이 없다는 고객 피드백을 마주할 때가 있습니다. 안드로이드 웹뷰는 기본적으로 파일 선택 요청만 던질 뿐, 실제 파일 탐색기를 띄우고 결과를 받아오는 UI 처리는 앱(Native)이 직접 중개해야 하기 때문입니다.

저의 경우도 최근 작업 중에 "파일 선택을 한 번 취소하고 나면, 그 다음부터 버튼이 아예 먹통이 되는 현상"을 겪었습니다. 원인을 파악해 보니 웹뷰의 파일 선택 콜백(ValueCallback) 생명주기 관리 문제였습니다.

나중에 또 같은 문제로 헤매지 않기 위해, 실제 서비스 적용이 가능한 구현 코드와 운영 시 꼭 챙겨야 할 주의점을 정리해 둡니다.

내가 겪은 문제와 원인

웹 페이지가 파일 선택을 요청하면 앱은 WebChromeClient.onShowFileChooser()에서 이 요청을 받게 됩니다. 이때 웹뷰는 결과를 기다리는 대기 상태로 들어가며, 앱은 ValueCallback<Uri[]> 객체에 사용자가 선택한 파일 경로를 담아 웹뷰에 돌려줘야 합니다.

여기서 가장 자주 하는 실수가 "사용자가 파일 선택을 취소했을 때 아무런 처리를 하지 않는 것"입니다. 콜백에 결과를 돌려주지 않고 그냥 끝나버리면, 웹뷰는 여전히 이전 요청을 대기하는 상태로 남아있게 되어 다음 번 클릭부터는 아예 반응을 하지 않게 됩니다.

해결 코드 (바로 적용하는 구현 예시)

최근 안드로이드 개발 표준에 맞춰 기존의 startActivityForResult 대신 Activity Result API(registerForActivityResult)를 사용해 구현한 코드입니다. 보안을 위해 실제 업로드 URL이나 로컬 파일 경로는 제외하고 틀만 잡았습니다.

Kotlin
class UploadWebViewActivity : AppCompatActivity() {
    private lateinit var webView: WebView
    private var fileCallback: ValueCallback<Array<Uri>>? = null

    // Activity Result API를 사용하여 파일 선택 결과 처리
    private val fileChooserLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult(),
    ) { result ->
        val callback = fileCallback
        fileCallback = null

        if (callback == null) return@registerForActivityResult

        // 선택된 결과 처리 (취소 시 null이 반환되어 웹뷰 대기 상태가 해제됨)
        val uris = WebChromeClient.FileChooserParams.parseResult(
            result.resultCode,
            result.data,
        )
        callback.onReceiveValue(uris)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_upload_webview)

        webView = findViewById(R.id.webView)
        webView.webChromeClient = object : WebChromeClient() {
            override fun onShowFileChooser(
                webView: WebView,
                filePathCallback: ValueCallback<Array<Uri>>,
                fileChooserParams: FileChooserParams,
            ): Boolean {
                // 1. 새 요청이 오면 기존에 남아있던 콜백부터 안전하게 정리
                fileCallback?.onReceiveValue(null)
                fileCallback = filePathCallback

                return runCatching {
                    // 웹뷰가 요청한 조건(MIME type 등)이 반영된 인텐트 생성
                    val intent = fileChooserParams.createIntent()
                    fileChooserLauncher.launch(intent)
                    true
                }.getOrElse {
                    // 에러 발생 시에도 콜백을 반드시 해제해야 먹통이 안 됨
                    fileCallback?.onReceiveValue(null)
                    fileCallback = null
                    Toast.makeText(
                        this@UploadWebViewActivity,
                        "파일 선택기를 열 수 없습니다.",
                        Toast.LENGTH_SHORT,
                    ).show()
                    true
                }
            }
        }
    }

    override fun onDestroy() {
        // 액티비티가 종료될 때 콜백이 남아있다면 정리
        fileCallback?.onReceiveValue(null)
        fileCallback = null
        super.onDestroy()
    }
}

코드 구현 핵심 포인트

  1. 이전 콜백의 사전 정리: 새로운 파일 선택 요청이 들어오면 fileCallback?.onReceiveValue(null)을 먼저 호출하여 혹시나 꼬여있을지 모르는 이전 상태를 초기화합니다.

  2. 예외 및 취소 처리 필수: 사용자가 뒤로가기를 누르거나 파일 선택 창을 그냥 닫았을 때, 혹은 에러가 났을 때도 반드시 onReceiveValue(null)을 호출해야 웹뷰의 먹통 현상을 막을 수 있습니다.

  3. fileChooserParams.createIntent() 활용: 웹 페이지에서 지정한 accept="image/*" 같은 MIME type 제한이나 다중 파일 선택(multiple) 옵션을 네이티브 인텐트에 그대로 넘겨주는 가장 안전한 방법입니다.

실무 운영 시 권한 및 카메라 처리 기준

단순히 기기 내부의 파일을 고르는 것을 넘어, 카메라 촬영까지 연동해야 한다면 설계 기준을 더 깐깐하게 잡아야 합니다.

  • 넓은 저장소 권한 요구 지양: 최근 안드로이드 버전 정책상, 시스템 파일 선택기(GetContent)나 포토 피커(Photo Picker)를 사용하면 앱에 별도의 미디어 읽기 권한을 요구하지 않고도 사용자가 선택한 파일의 content:// URI를 안전하게 획득할 수 있습니다. 굳이 불필요한 저장소 전체 권한을 요구하지 않는 것이 UX와 구글 플레이 심사 모두에 유리합니다.

  • 카메라 촬영 시 FileProvider 사용: 웹의 요청으로 카메라 앱을 띄워 직접 촬영한 결과를 넘겨야 할 때, 절대 로컬 파일 경로인 file://을 그대로 웹뷰에 노출하면 안 됩니다. 안드로이드 보안 정책에 위배되므로 반드시 FileProvider를 통해 임시 content:// URI를 생성하여 보안을 강화해야 합니다. 이때 생성하는 임시 파일명에도 사용자 식별 정보가 섞이지 않도록 주의합니다.

보안 및 운영 체크리스트

실제 운영 환경에 배포하기 전에 아래 리스트는 반드시 더블 체크하시는 것이 좋습니다.

점검 항목확인 내용
콜백 생명주기파일 선택 취소, 에러 발생, onDestroy 시점에 모두 onReceiveValue(null) 처리가 보장되는가?
권한 최소화불필요하게 넓은 저장소 권한을 요구하는 대신, 시스템 피커나 포토 피커를 우선 검토했는가?
로그 보안운영 로그(Logcat)나 에러 리포트에 실제 업로드 서버 URL, 사용자 토큰, 로컬 파일 경로가 노출되지 않는가?
카메라 보안촬영 결과물 전달 시 file://이 아닌 FileProvider 기반의 content:// URI를 사용하는가?
예외 케이스 테스트카메라 권한 거부, 기기에 카메라 앱이 없는 환경, 파일 선택 중 화면 회전 시에도 앱이 죽지 않는가?
교차 검증 (서버)앱 단계에서 MIME type(확장자)을 제한했더라도, 실제 업로드를 받는 서버 측에서 2차 검증을 수행하는가?

요약 및 마무리

안드로이드 웹뷰의 파일 업로드는 단순한 "웹 화면 연동"이 아니라 "앱이 사용자 파일 접근 권한을 안전하게 중개하는 보안 기능"으로 접근해야 마이너한 버그를 막을 수 있습니다.

특히 취소 처리가 누락되어 발생하는 '두 번째 클릭 먹통 버그'는 개발 단계에서 놓치기 쉬운 단골 서포트 요청 중 하나입니다. 위 가이드와 체크리스트를 기반으로 프로젝트 환경(Activity, Fragment 등)에 맞춰 안전하게 구현해 보시길 바랍니다. 자꾸 까먹으니 이렇게 기록으로 남겨둡니다.

댓글

이 블로그의 인기 게시물

안드로이드 화면 꺼짐 방지 FLAG_KEEP_SCREEN_ON 및 WakeLock 적용 방법

안드로이드 백그라운드 타이머 구현 및 Foreground Service 알림 설계 정리

관광 앱 다국어 데이터 모델 및 검색 색인 설계 (자꾸 까먹어서 정리)