Android WebView에서 파일 업로드 기능 구현하기

윤뿔소·2024년 6월 21일
0

목록 보기
2/2

오늘은 저번에 배포했던 Android WebView에서 파일 업로드 기능을 구현하는 과정을 공유하려고 한다.
정말 기분이 좋게도 레거시 버전의 코드를 이전 개발자가 주고 튀었다. 그래서 기능이 비는 부분이 있어 이렇게 구현해봤다.

파일 업로드 기능 구현하기

Android WebView에서 파일 업로드 기능을 구현하려면 중요한 단계를 거쳐야 한다. 권한 설정과 기존 Webview 설정을 건들기도 해야한다.

1. 권한 설정하기

파일 업로드를 하려면 필요한 권한을 AndroidManifest.xml 파일에 추가해야 한다. WRITE_EXTERNAL_STORAGE 권한은 구형 Android 버전에서만 필요했기 때문에 READ_EXTERNAL_STORAGE 권한만 추가한다.

user 아니다. uses다. 내가 헷갈려서 말하는 건 아니다.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

+ 사진을 보면 알겠지만 READ_EXTERNAL_STORAGE 권한은 Deprecated 됐다. 요즘 Android13+은 READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO 이렇게 권한을 부여하니 알아두길 바란다. 나는 Android 10 버전 이하도 지원해야하기 때문에 이렇게 하긴 했다.

2. WebView MainActivity 설정

이제 WebView를 설정한다. WebChromeClient를 설정하여 파일 선택을 처리할 수 있도록 한다. 애초에 웹뷰앱이 나오도록 MainActivity를 구성했다면 URL을 포함한 MainActivity 코드가 있을 것이다. 나는 내 기존 코드에서 출발해서 파일 업로드 관련 코드만 올리겠다.

그 외 mWebView, onBackPressed, onCreate 같은 클래스, 메소드 등등은 제외 하겠다. 흔하긴 하지만 아무래도 현업에 사용하는 코드다 보니.. onBackPressed 같은 구식도 있어서 현재 쓰기엔 적합하지 않는 코드도 있어서 추출해 보여주겠다.

기존 코드

public class MainActivity extends Activity {
	// 클래스 변수
	// onCreate 메소드
    
    ...

	// 1. MyWebChromeClient 클래스
    private class MyWebChromeClient extends WebChromeClient {
    	...
        
        @Override
        public boolean onShowFileChooser(WebView webView,
                                          ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
            System.out.println("WebViewActivity A>5, OS Version : " + Build.VERSION.SDK_INT + "\t onSFC(WV,VCUB,FCP), n=3");
            if (mFilePathCallback != null) {
                mFilePathCallback.onReceiveValue(null);
            }
            mFilePathCallback = filePathCallback;

            return true;
        }
	}

보는 것처럼 onShowFileChooser 메소드만 있고, onActivityResult은 다른 내용만 있지 파일 관련 코드가 없었다. 심지어 onShowFileChooser 메소드는 만들다 말았다! 이러니까 실행이 안되고 앱이 꺼지지!!
여기를 채워줘야했었다.

1. onShowFileChooser 메소드 (MyWebChromeClient)

이 메소드는 파일 선택기(intent)를 호출하여 사용자가 파일을 선택할 수 있도록 한다. <input type="file"> 태그가 클릭됐을 때 호출 된다!

과정은 아래와 같다.

과정

  1. 기존 파일 선택 콜백 조건부 초기화
    mFilePathCallback이 이미 설정됐다면 이를 null로 설정해 기존 파일 선택 콜백을 초기화한다. 이렇게 하면 이전 파일 선택 작업이 취소된다. 이 코드를 작성해 혹여나 생길 파일 겹침 문제를 예방한다.
if (mFilePathCallback != null) {
    mFilePathCallback.onReceiveValue(null);
}
  1. 파일 선택 콜백 설정
    초기화한 mFilePathCallback 변수에 onShowFileChooser 파라미터 중 하나인 filePathCallback를 할당한다.
mFilePathCallback = filePathCallback;
  1. 파일 선택기 인텐트 생성 및 실행
    또다른 파라미터인 FileChooserParams를 사용해 파일 선택기 인텐트를 생성한다. 그 후 startActivityForResult 메소드를 호출해 파일 선택기를 실행합니다.
Intent intent = fileChooserParams.createIntent();
try {
    startActivityForResult(intent, INPUT_FILE_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
    mFilePathCallback = null;
    Toast.makeText(MainActivity.this, "Cannot open file chooser", Toast.LENGTH_LONG).show();
    return false;
}
return true;

결과

잘 끝냈다면 안드로이드에 내장된 파일 호출기가 실행될 것이다.

private class MyWebChromeClient extends WebChromeClient {
    @Override
    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
        if (mFilePathCallback != null) {
            mFilePathCallback.onReceiveValue(null);
        }
        mFilePathCallback = filePathCallback;

        Intent intent = fileChooserParams.createIntent();
        try {
            startActivityForResult(intent, INPUT_FILE_REQUEST_CODE);
        } catch (ActivityNotFoundException e) {
            mFilePathCallback = null;
            Toast.makeText(MainActivity.this, "Cannot open file chooser", Toast.LENGTH_LONG).show();
            return false;
        }
        return true;
    }
}

2. onActivityResult 메소드

이 메소드는 사용자가 파일 선택을 완료 / 취소 후 호출된다.
알아둘 것이 나는 구버전의 Android 버전도 지원했어야하기 때문에 예외 처리 및 취소 시의 처리를 다양하게 접목 시켰다. 최신 버전만 지원하게 작성한다면 더 간단히 할 수 있다.

과정

예외 처리 다 빼고 일단 핵심만 작성하겠다.
1. 메소드 시작 / 부모 클래스 메소드 호출
super.onActivityResult(requestCode, resultCode, data);는 부모 클래스의 onActivityResult 메소드를 호출해 기본 동작을 수행한다.

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
  1. 파일 선택 요청 코드 확인
    requestCode가 파일 선택 요청 코드(INPUT_FILE_REQUEST_CODE)와 일치하는지 확인한다. 이 코드는 파일 선택 호출기를 열 때 사용한 코드와 일치해야 한다. 예외 처리를 위한 조건이다.
    // 파일 선택을 위한 요청 코드 확인
    if (requestCode == INPUT_FILE_REQUEST_CODE) {
  1. 파일 선택이 취소된 경우 처리
    resultCodeActivity.RESULT_OK가 아닌 경우다. 즉 사용자가 파일 선택을 취소했거나 오류가 발생한 경우 실행하는 이중 조건문이다.
    1. mFilePathCallbacknull이 아니면 null 값을 전달해 파일 선택이 취소됐다는 걸 콜백에 알린다.
    2. mUploadMessagenull이 아니면 null 값을 전달해 파일 선택이 취소됐다는 걸 알린다.
    3. 임시 파일을 담는 TempimageFile이 존재하고 삭제할 수 있다면 삭제한다.
    4. 메소드를 종료(return).
        // 사용자가 파일 선택을 취소했거나 결과가 OK가 아닌 경우
        if (resultCode != Activity.RESULT_OK) {
            if (mFilePathCallback != null) {
                // 파일 선택이 취소됐음을 콜백에 전달
                mFilePathCallback.onReceiveValue(null);
                mFilePathCallback = null;
            }
            if (mUploadMessage != null) {
                mUploadMessage.onReceiveValue(null);
                mUploadMessage = null;
            }
            if (TempimageFile != null && TempimageFile.exists()) {
                // 임시 파일 삭제
                TempimageFile.delete();
            }
            return; // 메서드 종료
        }
  1. 파일 선택 성공 처리
    실패 조건문 이후 로직이다. 여기에 도달했다면 성공으로 보기에 로직이 실행된다.
  • results 배열을 초기화.
  • 예외 처리 : datanull이거나 resultCodeRESULT_OK가 아니라면?
    • mCameraPhotoPathnull이 아니면 임시 파일의 경로를 Uri로 변환해 results 배열에 저장.
  • datanull이 아니라면?
    • data.getDataString()을 호출해 단일 파일 선택을 처리.
    • dataStringnull이 아니라면, Uri.parse(dataString)을 사용하여 Uri 객체를 생성하고 results 배열에 저장.
    • dataStringnull인 경우, 다중 파일 선택을 처리.
      • data.getClipData()null이 아닌 경우, 선택된 파일의 수(numSelectedFiles)를 가져오고, 각 파일의 Uriresults 배열에 저장!
        Uri[] results = null;

        // 파일을 성공적으로 선택한 경우
        try {
            if (data == null || resultCode != RESULT_OK) {
                // 데이터가 없거나 결과가 OK가 아닌 경우, 임시 파일 사용
                if (mCameraPhotoPath != null) {
                    results = new Uri[]{Uri.parse(mCameraPhotoPath)};
                }
            } else {
                String dataString = data.getDataString();
                if (dataString != null) {
                    // 단일 파일 선택 처리
                    results = new Uri[]{Uri.parse(dataString)};
                } else {
                    // 다중 파일 선택 처리
                    if (data.getClipData() != null) {
                        final int numSelectedFiles = data.getClipData().getItemCount();
                        results = new Uri[numSelectedFiles];
                        for (int i = 0; i < numSelectedFiles; i++) {
                            results[i] = data.getClipData().getItemAt(i).getUri();
                        }
                    }
                }
            }
        } catch (Exception e) {
            // 예외 처리 로그
            Log.e("MainActivity", "File select error", e);
        }
  1. 결과 전달
    선택된 파일의 경로를 콜백을 통해 전달한다.
  • 예외 처리 : mFilePathCallbacknull이 아닌 경우, results 배열을 전달하여 콜백을 호출.
  • mFilePathCallbacknull로 설정.
  • mFilePathCallbacknull이고, mUploadMessagenull이 아니라면?
    • results 배열의 첫 번째 Uri를 전달해 콜백을 호출.
    • mUploadMessagenull로 설정.
        // 결과가 null이 아니면 콜백에 Uri 배열 전달
        if (mFilePathCallback != null) {
            mFilePathCallback.onReceiveValue(results);
            mFilePathCallback = null;
        } else if (mUploadMessage != null) {
            // 구버전 API를 위한 처리
            mUploadMessage.onReceiveValue(results[0]);
            mUploadMessage = null;
        }
    }
}

+ mUploadMessage는 구버전 안드로이드(4.4 이전)에서 파일 선택 콜백을 처리하기 위한 변수다. 누가 4.4를 쓰냐고 묻는다면 할 말 없지만 일단 해놨다.

전체 코드

이렇게 onActivityResult 메소드는 파일 선택 호출기에서 반환 결과를 처리 후, 선택된 파일의 경로를 콜백을 통해 전달하는 역할을 한다.

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    // 파일 선택을 위한 요청 코드 확인
    if (requestCode == INPUT_FILE_REQUEST_CODE) {
        // 사용자가 파일 선택을 취소했거나 결과가 OK가 아닌 경우
        if (resultCode != Activity.RESULT_OK) {
            if (mFilePathCallback != null) {
                // 파일 선택이 취소됐음을 콜백에 전달
                mFilePathCallback.onReceiveValue(null);
                mFilePathCallback = null;
            }
            if (mUploadMessage != null) {
                mUploadMessage.onReceiveValue(null);
                mUploadMessage = null;
            }
            if (TempimageFile != null && TempimageFile.exists()) {
                // 임시 파일 삭제
                TempimageFile.delete();
            }
            return; // 메서드 종료
        }

        Uri[] results = null;

        // 파일을 성공적으로 선택한 경우
        try {
            if (data == null || resultCode != RESULT_OK) {
                // 데이터가 없거나 결과가 OK가 아닌 경우, 임시 파일 사용
                if (mCameraPhotoPath != null) {
                    results = new Uri[]{Uri.parse(mCameraPhotoPath)};
                }
            } else {
                String dataString = data.getDataString();
                if (dataString != null) {
                    // 단일 파일 선택 처리
                    results = new Uri[]{Uri.parse(dataString)};
                } else {
                    // 다중 파일 선택 처리
                    if (data.getClipData() != null) {
                        final int numSelectedFiles = data.getClipData().getItemCount();
                        results = new Uri[numSelectedFiles];
                        for (int i = 0; i < numSelectedFiles; i++) {
                            results[i] = data.getClipData().getItemAt(i).getUri();
                        }
                    }
                }
            }
        } catch (Exception e) {
            // 예외 처리 로그
            Log.e("MainActivity", "File select error", e);
        }

        // 결과가 null이 아니면 콜백에 Uri 배열 전달
        if (mFilePathCallback != null) {
            mFilePathCallback.onReceiveValue(results);
            mFilePathCallback = null;
        } else if (mUploadMessage != null) {
            // 구버전 API를 위한 처리
            mUploadMessage.onReceiveValue(results[0]);
            mUploadMessage = null;
        }
    }
}

나는 이렇게 하니 너무너무 잘됐다! 그 카타르시스가 아직도 생각이 난다.

이렇게 보니 너무 못생기긴 했는데.. 개발할 당시 너무 에러가 나서 이것 저것 붙이다 보니 이렇게 된 거 같다. 뭐 GPT도 그럴 수 있다 했고, 필요한 조건문이라 어쩔 수 없다. 나중에 메소드 분리 리팩토링을 해야할 거 같다.

결론

이렇게 해서 Android WebView에서 파일 업로드 기능을 구현했다. 이번 도전을 통해 파일 업로드 기능을 구현할 수 있었다. 다양한 에러 상황을 배제하는 것도^^,, 이걸 읽는 독자도 도움이 됐길 빈다.

profile
코뿔소처럼 저돌적으로

0개의 댓글