본문 바로가기

Android/WebView

[WebView] WebView 사용 방법 - 2. WebClient 및 다중 JavaScriptInterface 사용.

728x90

본 게시글은 이전 게시글에 이어서 작성된 부분입니다.

2022.04.06 - [Android/WebView] - [WebView] WebView에서 JavaScriptInterface를 사용해보자.

 

전에 작성했던 webView 부분을 조금 더 확장해서 기능을 추가해볼 예정이다.

기본으로 사용했던 Client를 간단하게 커스텀해보고, 2개의 javaScriptInterface를 사용할 수 있는 구조로 변경해 보았다.


우선 이전에 사용했었던

WebViewClient, WebChromeClient를 상속받는 클래스를 만들어서 해당 부분을 커스텀해보았다.

 

 

 

이처럼 Base에 해당하는 패키지에 작성해 두었고, 현재 많은 부분을 커스텀할 수는 없기 때문에 많이 사용하는 부분만 Override 하여서 사용해 보았다.

 

class BaseWebViewClient(vm: WebViewModel) : WebViewClient() {
    private var viewModel: WebViewModel? = null

    init {
        viewModel = vm
    }

    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        super.onPageStarted(view, url, favicon)
        viewModel!!.showProgress()
    }

    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        viewModel!!.hideProgress()
    }

    override fun shouldOverrideUrlLoading(
        view: WebView?,
        request: WebResourceRequest?
    ): Boolean {
        return super.shouldOverrideUrlLoading(view, request)
    }

    // WebView Error Control
    override fun onReceivedError(
        view: WebView?,
        request: WebResourceRequest?,
        error: WebResourceError?
    ) {

        // custom exceptionThrow. Check ERROR_*
        when (error?.errorCode) {
            ERROR_BAD_URL -> { }
            ERROR_CONNECT -> { }
            ERROR_UNKNOWN -> { }
            else -> { }
        }
        super.onReceivedError(view, request, error)
    }
}

 

처음으로 확인할 부분은 WebViewClient이다.

 

 

 

WebViewClient를 사용하는 이유는 간단하게 말해서 WebView가 Load 될 때 Notification을 받기 위해서이다.

즉, WebView가 불려질 때 발생하는 callback을 받아서 컨트롤할 수 있는 부분으로 많이 사용하는 부분이 위의 코드에 선언한 4가지 함수라고 생각한다.

 

각 함수에 대하여 간단하게 알아보면,

 

  • onPageStarted : WebView Load가 시작될 때 호출된다.
  • onPageFinished : WebView Load가 끝날 때 호출된다.
  • shouldOverrideUrlLoading : WebView 내부에서 다른 웹 url이 호출될 때 호출된다.
  • onReceivedError : WebView에서 error가 발생될 때 호출된다.

 

필자가 여기서 추가적으로 작업한 부분은,

  1. onPageStarted, Finished에 progress를 보여주고 숨기는 작업을 추가해 주었다.
  2. Error에 대한 함수를 추가하였다.

웹뷰에서 보여지는 데이터가 많을 때 생각보다 로드되는 시간이 오래 걸리는데, 이것을 로딩 중임을 표현해주기 위해서 프로그레스를 사용하고, 이를 webView Load의 시작과 끝에 show, hide를 추가하였다.

 

이때, BaseViewModel에 선언한 함수를 사용하기 위하여 ViewModel을 인자로 받았으며, BaseViewModel을 인자로 받을 수 있음에도 WebViewModel을 인자로 받은 이유는, 추후에 webViewModel의 데이터를 컨트롤하는 경우가 생길 가능성을 생각해서 더 넓은 범위의 ViewModel을 인자로 받아두었다.

 

ReceivedError에When 절을 추가하여 Error에 따른 처리를 추가할 수 있도록 작성하였다.

error.errorCode를 보면 필자가 작성한 3가지를 제외하고도 상당히 많은 에러코드를 확인할 수 있는데, 상황이나 필요에 따라서 추가하여 에러에 대한 처리를 이곳에서 작업하면 될 것으로 보인다.

 

실제 프로젝트에서 사용할 때는, shouldOverrideUrlLoading 부분을 커스텀하여 작업을 진행하는 부분이 많을 것으로 보인다.

 

다음으로,

WebChromeClient를 확인해보자.

 

class BaseWebChromeClient(vm: WebViewModel) : WebChromeClient() {
    private var viewModel: WebViewModel? = null

    init {
        viewModel = vm
    }

    override fun onCreateWindow(
        view: WebView?,
        isDialog: Boolean,
        isUserGesture: Boolean,
        resultMsg: Message?
    ): Boolean {
        return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg)
    }

    override fun onCloseWindow(window: WebView?) {
        super.onCloseWindow(window)
    }

    // WebView Loading Check newProgress : 0 to 100
    override fun onProgressChanged(view: WebView?, newProgress: Int) {
        super.onProgressChanged(view, newProgress)
        Log.d("newProgressCheck" , "now ? $newProgress")
        viewModel!!.progressPercent(newProgress.toString())
    }

    override fun onJsAlert(
        view: WebView?,
        url: String?,
        message: String?,
        result: JsResult?
    ): Boolean {
        return super.onJsAlert(view, url, message, result)
    }

    override fun onJsConfirm(
        view: WebView?,
        url: String?,
        message: String?,
        result: JsResult?
    ): Boolean {
        return super.onJsConfirm(view, url, message, result)
    }
}

 

WebViewClient가 로딩될 때에 대한 callBack을 처리했다면, WebChromeClient는 WebView 로드가 끝나고 발생하는 이벤트에 대한 처리를 해주는 부분이다.

 

이 부분에서도 필자가 override 한 부분에 대해 간단하게 알아보자.

 

  • onCreateWindow : WebView에서 새 창을 열 때 호출
  • onCloseWindow : WebView에서 창을 닫을 때 호출
  • onProgressChanged : WebView가 loading 될 때 호출
  • onJsAlert : WebView에서 Alert()이 호출되면 호출
  • onJsConfirm : WebView에서 confirm()이 호출되면 호출

 

각 함수들의 이름만 보아도 어떠한 상황에 호출되는 것인지 알 수 있다.

여기서, 하단의 2개는 Dialog와 같은 창을 띄울 때 WebView에서 호출하는 함수WebView에서 직접 호출하지 않는다면 해당 부분을 override 할 필요는 없다.

 

필자는 이 부분에서 onProgressChanged에서 newProgress라는 값을 사용해 보았다.

해당 함수는 웹뷰 로드가 끝나고, 해당 웹뷰에서 호출되는 이벤트를 컨트롤하는 다른 함수와 다르게 웹뷰가 로딩될 때 호출되는 부분이다.

WebViewClient에서 onPageStarted와 onPageFinished의 사이에 호출되는 부분으로, newProgress라는 값으로 몇 퍼센트 로드가 완료됐는지 확인할 수 있다.

 

 

위에서 언급했듯이 WebView를 로딩할 때 생각보다 오랜 시간이 걸릴 때가 있으므로, 해당 값을 사용하여 몇 퍼센트 로딩이 되었는지 확인하는 용도로 사용해보았다.

 

 

간단하게 확인을 위해 TextView를 사용하였지만, 해당 값에 따라 이미지를 변경한다던지 등의 작업을 통해 좀 더 자연스럽고 높은 퀄리티의 progress를 만들 수 있을 것으로 보인다.

 

해당 부분 외에도, onJsAlert도 실제 웹뷰가 들어가는 프로젝트에서 자주 사용할 것으로 보인다.

커스텀 된 Alert을 띄우기 위해서 해당 부분에서 별도의 처리를 하는 등 의 작업으로 말이다.


다음으로는,

앞서 선언했던 JavaScriptInterface 외에 다른 Interface를 선언하여 사용해보려고 한다.

 

필자가 이렇게 JavaScriptInterface를 여러 개 사용하려는 이유는,

모든 기능이 하나로 통합된 WebView를 만들고 싶지 않기 때문이다.

 

몇 개 안 되는 부분에서 WebView를 사용하고 JavaScriptInterface의 개수가 별로 되지 않는다면 상관없겠지만,

사용되는 범위가 넓어질수록 WebView 자체가 무거워지고, 불필요한 소스코드가 많아질 것으로 보이기 때문이다.

물론, 이것도 사용하는 프로젝트의 구조 설계 단계에서 기획을 보고 판단해야겠지만 말이다.

 

우선 필자가 생각할 때, 방향성이 다른 WebView는 각각의 Activity를 가지고 있어야 한다고 생각한다.

 

하지만 지금은 샘플 앱이기 때문에 하나의 WebView를 사용하고, 호출되는 URL에 따라서 다른 JavaScriptInterface를 사용하도록 하고자 한다.

 

 

임시로 사용할 JavaScriptInterface와 Repository를 선언한 후에,

 

class WebViewModel : BaseViewModel(), JavaScriptRepository, DummyRepository

 

WebViewModel에 상속받아서 사용하도록 한다.

 

기존에 사용하는 방법과 동일하게 상속받은 repository를 override 해주어서 사용하면 되는데,

여기서 생각이 필요한 부분은 WebView의 세팅 부분이다.

 

javaScriptInterface = JavaScriptInterface()
javaScriptInterface!!.repository = this@WebViewModel
addJavascriptInterface(javaScriptInterface!!, "JavaScriptInterface")

 

이전 게시글에서 WebView에 대한 설정을 할 때, JavaScriptInterface를 위와 같이 설정을 해주었다.

 

하지만, 지금은 URL에 따라서 한 개의 Interface가 아닌 여러 개의 Interface를 필요에 따라 변경하여 사용해야 한다.

따라서, 다른 JavaScriptInterface를 필요로 하는 페이지로 loadUrl 할 때 해당 부분을 변경해주어야 한다.

 

removeJavascriptInterface("JavaScriptInterface")
dummyInterface = DummyScriptInterface()
dummyInterface!!.repository = this@WebViewModel
addJavascriptInterface(dummyInterface!!, "DummyScriptInterface")
webView!!.loadUrl(arg)

 

이처럼, loadUrl 전에 removeJavaScriptInterface를 통해 등록된 interface를 지워주고, 새로운 Interface를 선언해서 등록해주는 작업이 필요하다.

 

하지만, 해당 부분을 loadUrl 호출 시마다 작성하기엔 중복 코드가 많아지므로,

 

fun initJavaScriptInterface(jsInterface: String) {
    if (nowInterface != null) {
        webView!!.removeJavascriptInterface(nowInterface!!)
    }

    // JavaScriptInterface가 추가될 때 마다 조건 추가 필요.
    when (jsInterface) {
        "JavaScriptInterface" -> {
            webView!!.run {
                javaScriptInterface = JavaScriptInterface()
                javaScriptInterface!!.repository = this@WebViewModel
                addJavascriptInterface(javaScriptInterface!!, nowInterface!!)
            }
        }
        "DummyScriptInterface" -> {
            webView!!.run {
                dummyInterface = DummyScriptInterface()
                dummyInterface!!.repository = this@WebViewModel
                addJavascriptInterface(dummyInterface!!, nowInterface!!)
            }
        }
        else -> {
            Log.d("javaScript", "initJavaScriptInterface not found")
        }
    }
}

 

따로 함수를 빼내어서 작업하도록 한다.

 

여기까지만 하면 url이동에는 정상적으로 동작을 하는데, 뒤로 가기를 눌렀을 때 문제가 발생한다.

 

A > B > C와 같이 앞으로 갈 때 interface를 재 설정해주었기 때문에 정상적으로 동작했지만,

BackKey를 통해 뒤로 가는 경우에 이전에 사용한 Interface를 다시 설정해주어야 한다.

 

따라서, WebViewModel에서 loadUrl이 호출될 때마다 interface에 대한 정보를 저장해주어서 해당 값을 사용하도록 해주었다.

 

viewModelScope.launch {
    interfaceList.add(nowInterface!!)
    nowInterface = "DummyScriptInterface"
    initJavaScriptInterface(nowInterface!!)
    webView!!.loadUrl(arg)
}

 

이처럼 interfaceList라는 ArrayList를 선언하고 사용할 interface의 이름을 저장해주도록 하였다.

해당 값을 저장한 이유는, 앞서 선언한 initJavaScriptInterface라는 함수 Back Key를 눌렀을 때도 사용하여 webView 설정을 변경하기 위해서이다.

 

override fun onBackPressed() {
    if (webView != null && webView!!.canGoBack()) {
        val size = viewModel.interfaceList.size
        val value = viewModel.interfaceList[size - 1]

        viewModel.interfaceList.removeLast()
        viewModel.initJavaScriptInterface(value)
        webView!!.goBack()
    } else {
        finish()
    }
}

 

WebViewActivity에 onBackPressed()를 override 하여 back키를 눌렀을 때 WebView를 종료하는 것이 아닌 이전 webView를 보여주도록 한다.

 

저장한 interface Name을 가져오고, removeLast를 통해 가져온 이름을 제거한 후에 initJavaScriptInterface를 호출하여 webView 설정을 바꾼 후에 goBack을 통해 이전 화면으로 넘어가도록 한다.

 

여기서, 

canGoBack과 goBack은 webView에서 지원하는 함수로, History가 있다면 canGoBack이 true 값이 되고, goBack을 통하여 이전 History로 이동이 가능하다.


필자는 위의 2가지 JavaScriptInterface를 테스트하기 위하여 2가지의 html 파일을 사용했는데,

실제로 웹 페이지를 사용하는 경우에는 추가적으로 작업해야 하는 부분이 있다.

 

override fun shouldOverrideUrlLoading(
    view: WebView?,
    request: WebResourceRequest?
): Boolean {
    viewModel!!.interfaceList.add(viewModel!!.nowInterface!!)
    return super.shouldOverrideUrlLoading(view, request)
}

 

 

BaseWebViewClient 부분의 shouldOverrideUrlLoading 부분이다.

WebView가 오픈된 상태에서, JavaScriptInterface를 통한 페이지 이동이 아닌 자체적인 url 변경의 경우 해당 부분이 호출되게 된다.

따라서 url이 변경되면서 History는 추가되는 반면, 뒤로 가기를 눌렀을 때 onBackPressed를 통해 제거할 interfaceList는 추가되지 않아서 문제가 발생하게 된다.

그렇기 때문에 shouldOverrideUrlLoading이 호출될 때도 해당 부분이 호출되는 JavaScriptInterface가 한번 더 추가되도록 add 해주는 부분이 필요하게 된다.


작업을 진행하다 보니, 특정 URL로 이동할 때 JavaScriptInterface를 변경하는 경우가 더 많을 것으로 보인다.

 

테스트를 위해 현재 html 파일에 따라 다른 javaScriptInterface를 사용하도록 하였는데, 실제로 매번 변경되는 경우는 드물 것으로 생각된다.

각 WebView 종류마다 하나의 Interface가 존재하고, 다양한 곳에서 접근이 가능한 Web Page 같은 경우 공통적으로 사용되어야 하기 때문에 이런 경우 JavasScriptInterface를 변경해 사용하는 방식으로 구현이 되지 않을까 싶다.

 

728x90