본문 바로가기

Android/Jetpack Compose

[Compose] Compose환경 WebView에서 JavascriptInterface를 사용할 때 주의할 점

728x90

필자가 실무를 진행하면서 Compose 환경에서 webView를 사용해야 할 케이스가 생겼다.

webView는 여러모로 많이 사용해 보았기 때문에 아주 쉽게 구현하여 확인하고 끝낼 생각이었지만, 생각지도 못한 부분에서 오류가 발생하여 해결하는데 시간을 좀 소모하였다.

 

명확하게 원인을 파악한 것은 아니지만, 같은 문제를 해결하고 있는 사람들을 위해 문제 점과 해결한 방법에 대해서 작성해보고자 한다.


우선 문제점은 다음과 같다.

 

웹뷰를 선언하여 특정 버튼을 눌렀을 때, webView를 열어 이후 프로세스를 진행하도록 해야 한다.

여기서 webView에서 android로 호출하는 javascriptInterface는 정상적으로 동작하고 있었으나, android에서 webView로 호출하는 javascriptInterface를 webView 측에서 정상적으로 받아오지 못하고 있었다.

 

문제만 보면, web 쪽에서 무언가 처리를 잘못한 것 아닌가? 싶을 수 있다.

필자도 동일하게 생각했었지만,

web 쪽 코드를 아무리 확인해 봐도 문제점을 찾을 수 없었고, 다른 webView를 사용하는 부분에서는 정상적으로 동작하고 있기 때문에 android 내부적으로 작성한 코드에 문제가 있다는 것을 알 수 있었다.

@Composable
fun WebView(
    modifier: Modifier = Modifier,
    url: String,
    subUrl: String,
    ...
) {
    var webView: WebView? = null
    val webViewState = rememberWebViewState(url = "$url$subUrl")
    val webViewClient = AccompanistWebViewClient()
    val context = LocalContext.current
    val webChromeClient = remember { ClassUWebChromeClient(context) }

    ...

    val photoLauncher = rememberPhotoLauncherForActivityResult { uri ->
        uri?.let {
            viewModel.uploadImage() {
                uploadImageWebView() { jsonObject ->
                    webView?.loadUrl("javascript:webViewFileUploadCallback('$jsonObject')")
                }
            }
        }
    }

    WebView(
        state = webViewState,
        client = webViewClient,
        chromeClient = webChromeClient,
        onCreated = { webView ->
            with(webView) {
                webView = this

                settings.run {
                    ...
                }

                addJavascriptInterface(
                    object {
                        @JavascriptInterface
                        fun webViewFileUpload() {
                            ...
                            photoLauncher.launch(albumIntent)
                        }
                    }, "android"
                )

                ...
            }
        }
    )
    ...
}

 

코드는 간단하게 이와 같이 되어있다.

기본적인 세팅을 해주고, javascriptInterface를 설정하여 받을 수 있게 말이다.

 

동일한 webView Component를 사용하고 있기 때문에 어디에서는 되고, 어디에서는 안된다는 문제가 이 webView Component에 있다고 생각이 들지 않았다.

 

따라서, 해당 webView를 호출하는 부분부터 확인을 하였는데 다른 점은 하나 찾을 수 있었다.

 

recomposition이 발생하는가, 발생하지 않는가.

 

정상적으로 동작하는 부분은 recomposition이 발생하지 않았고, 동작하지 않는 부분 recomposition이 발생하고 있었다.

아무리 생각해도 이것이 문제처럼 생각될뿐더러, 불필요한 recomposition을 발생시키는 것은 좋지 않은 구조이기 때문에 이것을 먼저 수정하도록 하였다.

 

결과부터 말하면,

이 recomposition을 발생시키는 부분을 제거하고 난 후 동작을 확인해 보니 정상적으로 동작하는 것을 확인할 수 있었다.


그렇다면, 이것이 문제긴 문제인데 어째서 이런 일이 발생했는가?

 

필자는 recomposition이 발생해도 문제가 생길 것이라고 생각하지 못한 이유는 다음과 같다.

  1. webView Component는 url 기반으로 다시 그려지기 때문에 같은 url을 호출하는 경우 webView Component는 변화하지 않는다.
  2. javascriptInterface는 webView Component에 설정하는 부분이기 때문에, webView Component가 변화하지 않는다면 해당 javascriptInterface에도 변화가 없을 것이다.

하지만 이렇게 생각했음에도 불구하고 문제는 발생했다.

그래서 필자가 추측한 것은 다음과 같다.

  1. webView Component는 그대로 사용될지라도, webViewComponent를 감싸고 있는 WebView 함수가 다시 그려지면서 주소값이 변경될 것이다.
  2. 최초 연결 한 webView Component의 주소값이 A일 때, 다시 그려지고 난 후에는 A가 아니게 되므로 받아오는 javascriptInterface는 정상적으로 받아올 수 있으나 android에서 web으로 호출하는 경우 새롭게 생성된 다른 주소값이 아닌 맨 처음에 생성된 A로 호출이 되는 것이다.
  3. 실제로는 2개의 webView 객체가 생성되고, 보고 있는 webView와 내부적으로 함수가 동작하는 webView가 다른 객체가 되어 정상적으로 동작하지 않는 것이다.

이러한 가정을 세우고 log를 찍어보도록 하였다.

 

webView Component의 내부와 WebView 함수에서 각각 view의 hashCode를 찍어 보았다.

 

recomposition이 반복적으로 일어나는 경우의 로그는 다음과 같다.

 

우선 확인할 수 있는 부분은 

Webview 함수가 반복적으로 다시 그려지더라도, webView Component는 단 한 번만 Create를 타게 된다.

webView Component 안과 밖에서 호출한 hashCode들이 다르다.

 

그렇다면 recomposition이 발생하지 않는 경우의 로그는 어떨까? 안과 밖에서 호출하는 부분이 동일할까?

 

어림없었다.

 

한 번씩만 호출되기는 하지만, 이 두 경우의 hashCode 값 또한 같지 않다.

또한, recomposition이 발생하여 여러 번 WebView 함수가 다시 그려지더라도 반복하여 호출되는 hashCode 값을 변경되지 않고 일정하기 때문에 이는 동일한 객체를 사용하고 있다는 것을 알 수 있다.

 

따라서 생각했던 추측은 틀린 것처럼 보인다.

그래서 이번엔, WebView 함수가 다시 그려질 때마다 webView Component를 새로 생성하도록 변경을 해보았다.

 

동일한 url을 사용하고, webView의 onCreate를 다시 타도록 하여 새로운 객체를 만들었다.

webView Component 안에 있는 hashCode 값이 변경되는 것을 확인할 수 있었으며 정상적으로 javascriptInterface가 동작하는 것도 확인할 수 있었다.

 

webView Component의 hashCode는 변경되나 WebView 함수에서 찍어둔 로그의 hashCode는 변경되지 않는 것으로 보아, 새롭게 생성하도록 만들어도 해당 Compose 함수는 새롭게 생성되는 것이 아니라 recomposition이 발생하여 다시 그려지는 것이므로 변경되지 않는 것으로 보인다.

 

이 두 가지 테스트로 알 수 있는 것은 다음과 같다.

  1. webView Component는 한 번만 생성될지라도, recomposition이 발생하면 javascriptInterface를 호출할 때 문제가 생긴다.
  2. recomposition이 발생할지라도, webView Component가 매번 다시 생성된다면 javascriptInterface를 호출할 때 문제가 없다.

사실 2번과 같은 경우, recomposition이 발생하기는 하지만 매번 새롭게 생성되기 때문에 이는 recomposition이 발생하지 않은 것과 같게 처리된다. 따라서, 1번과 2번 모두 recomposition을 제거한 경우이고 이 경우 해결이 된 것이다.

 

원인은 정확하게 파악하지 못하였지만, 결과적으로 recomposition이 발생하면서 내부적으로 어떠한 데이터가 변경되어 javascriptInterface를 호출했을 때 web 쪽에서 정상적으로 받아오지 못하는 것으로 추정된다.

물론 webView의 모든 데이터를 전부 찍어보았을 때 hashCode가 같을지라도 다른 데이터가 변경될 가능성은 없지 않다.

하지만 그렇게까지 모든 데이터를 찾아볼 필요성은 아직 느끼지 못하였고, 어찌 되었던 문제를 추정하고 해결하였기 때문에 그렇게 까지 확인은 하지 않았다.


해당 이슈를 정리하면 다음과 같다.

  1. recomposition이 발생하게 되면 webView가 새롭게 생성되지 않는 경우 javascriptInterface를 호출할 때 정상적으로 동작하지 않을 수 있다.
  2. recomposition 발생 시 webView를 새롭게 생성하는 등 recomposition이 발생되지 않도록 수정하면 해당 문제를 해결할 수 있다.

정확하게 원인을 파악하지 못했다는 것이 조금 찝찝하기는 하지만,

애초에 불필요한 recomposition이 발생하고 있었던 것 자체만으로도 퍼포먼스에 좋지 않기 때문에 겸사겸사 함께 해결이 되어 다행이라고 생각한다.

 

같은 객체를 사용하는 것으로 보이고 Create시에 설정하는 javascriptInterface임에도 불구하고, 이러한 문제가 발생할 수 있다는 부분이 신기하기도 하며 어이가 없기도 하다.

 

당연히 문제가 없겠지 하고 안일하게 생각하여 간단하게 구현했던 webView 덕분에 이러한 문제가 생길 수 있다는 것을 알게 되어 다행이라고 생각하며, compose를 1년 넘게 사용하고 있음에도 불구하고 아직까지도 많은 부분을 놓치고 있다고 생각하여 꾸준히 공부를 해야겠다고 생각했다.

 

원인도 불명확한 이슈였지만, 필자와 동일한 이슈를 마주하고 있다면 이러한 방식으로 해결이 가능하다는 것을 보여주기 위하여 해당 글을 작성하였다.

물론 필자가 추측한 부분이 틀릴 수 있고 얻어걸려서 해결된 것일 수 있으므로, 다른 케이스가 있다면 댓글로 남겨주면 확인하여 글을 수정하도록 하겠다.

728x90