Tempo Di Valse

[Android] Web 에서 App 으로 데이터를 받아보자 본문

개발/Android

[Android] Web 에서 App 으로 데이터를 받아보자

TempoDiValse 2022. 2. 4. 17:52

 

Javascript랑 WebView를 연동하다보면 데이터를 주고 받는거는 필수적인 사항이다. 특히 프레임워크를 사용하는 사람이 아니라면 일일히 Web과 App에 대해 통신 하는 것을 다 짜야되는 수고가 있다. 그 전에 어떻게 Web과 App을 연결하느냐가 더 고민 일 수 있겠다. 둘이 독립적인 것이 아닌 가 생각을 하지만 역시나 길은 있으니 그거슨 바로 JavascriptInterface라는 것이 해결을 해 줄 것이다.

 

JavascriptInterface는 Web이 App에 접근할 수 있는 유일한 통로라고 보면 될 것 같다. 해당 Interface를 통해 App에 정의한 메소드를 호출할 수 있고 값을 던져줄 수 있게 된다.

 

예를 들어 Web에다가 이쁘게 카메라 버튼을 코딩해 넣은 것을 onclick 이벤트를 통해 카메라를 켜달라고 호출을 하면,

<button onclick="openCamera()">Camera</button>
<script>
    function openCamera(){
         // 켜줘.
    }
</script>
 

이런 형식으로 부르게 되는데 Web에서는 Native의 카메라를 호출하는 그런 신박한 Javascript 메소드는 존재하지 않는다. 그러면 어떻게 해야 하는가... 바로, App에서 전달 받아 알아들을 수 있게 코딩을 해야 하는 것이다.

fun openCamera(){
    val intent = Intent(???????) // 어떻게든 띄움
    startActivity(intent)
}
 

하지만 저렇게 이름만 맞춘다고 되는 것이 아니라 Web에서 App에 접근할 수 있도록 다리를 놓아주어야 하는데 그 다리 역할을 하는 것이 JavascriptInterface라는 것이다.

 

이번 포스팅에서는 JavascriptInterface를 통해 쉽게 메소드들을 관리하게 된 방법에 대해 작성해 보려 한다.


1. Javascript에서 어떻게 호출해야 할 지 고민하기

먼저 앱에 있는 기능을 JavascriptInterface를 가지고 사용한다고 했으면, JS단에서 어떻게 호출해야 할 지 고민을 했었다.

 

특히 내가 Android만 하는 것이 아니라 iOS를 함께 고민하고 있어야 될 포지션이라고 한다면 더 심도(?)있게 고민해야 한다. Android, iOS 두 개에서 보여준다고 해서 두 개의 컴포넌트를 만드는 번거로운 짓을 하기 보다는 하나의 메소드에서 분기처리를 할 수 있도록 하는 것이 깔끔하기 때문이다. 또한, JS 단에서 Interface를 하나하나 정의하기에는 관리가 너무 어렵다는 점이다.

 

그래서 이 두 OS를 한번에 처리할 수 있는 Sender를 만들었고, 메소드를 관리하기 편하게끔 호출형식에 대해서도 통일을 시켰다.

const AppInterface = (() => {
    'use strict';
    
    const PROTOCOL = "app_bridge";
    let target = 'N';

    AppInterface.prototype.send = (funcName, args) => {
        const form = {
            funcName: funcName,
            data: args || null
        };

        const _args = JSON.stringify(form);

        if(target === 'A'){
            window[PROTOCOL].dispatcher(_args);
        }else if(target === 'I'){
            window.webkit.messageHandlers[PROTOCOL].postMessage(_args);
        }else{
            throw "Not App Environment";
        }
    }

    function AppInterface(){
        const userAgent = window.navigator.userAgent;
        
        /* userAgent를 구분할 수 있는 로직 */
    }
})
 

간단하게 설명을 하자면,

 

A. userAgent 값을 통해서 이게 Android인지 iPhone인지 확인을 하게 된다. 앱 단에서 Custom UserAgent를 이용하여 더 편하게 정의할 수 있으면 좋을 것이다.

B. 공통의 메소드인 send를 정의했는데, 이 send는 앱의 메소드 이름만을 호출할 수 있도록하며 파라미터는 Object형으로 넘기도록 했다. 이렇게 되면 JS 에서

() => {
    const env = new AppInterface();

    env.send('appFunc1', 1); // 값 하나를 보내거나
    env.send('appFunc2', { 
        key: 'value'
    }) // 여러 데이터를 보내거나
}
 

다양하게 보내고 싶은 파라미터를 함께 실어버릴 수도 있을 것이다. 대신에 한계점이라 한다면, 여러 데이터를 보낼 때에는 무조건 JSON 형식으로 보내야 한다는 것이다. 왜냐하면 Android에서는 dispatcher라는 곳에서 모든 것을 처리할 예정이고, iOS에서는 WKWebView에서 MessageHandler를 관리하는 Delegate에서 모든 것을 처리할 예정이기 때문이다. 둘 다 이미 정해진 메소드 내에서 호출을 퍼트리기 때문에 첫번째 파라미터는 "메소드이름", 두번째 파라미터는 "해당 메소드에 전송할 파라미터"를 집어넣는 것이다.

 

이렇게 JS단에서 구축을 끝냈더라면, Android단에서는 이것을 받을 수 있도록 JavascriptInterface를 작성하도록 한다.

 

2. JavascriptInterface 작성

보통의 JavascriptInterface를 작성하게 되면,

WebView.addJavascriptInterface(object : JavascriptInterface {
    @android.webkit.JavascriptInterface
    fun refresh(){
        WebView.reload()
    }
}, "androidbridge")
 

이러한 형식으로 작성되는데 큰 귀차니즘은 메소드를 정의할 때 Annotation을 계속 써줘야 한다. 원하는 메소드를 호출 하기 위해서는 @android.webkit.JavascriptInterface가 정의된 메소드를 계속 호출하여야 한다는 것이었다.

 

Annotation을 써주지 않은 메소드는 JS단에서 정의되지 않은 메소드라고 호출을 거부하게 되는데, 그렇다고 각각 메소드마다 써주는 것은 내 관점에서는 그냥 소스가 안 이쁘다. 이게 내가 한 곳에서 메소드를 관리해야 하는 이유 중에 하나인 것이다.

 

그래서 생각했던 방식은 Abstract Class 화 해서 관리하는 것이다.

abstract class JSInterface {
    companion object {
        const val INTF = "app_bridge" // JS에서 PROTOCOL에 있는 값과 맞춘다
    }

    private var form : WebViewDataForm? = null

    /**
        Web에서 들어오는 모든 처리는 dispatcher 메소드에서 이루어진다
    */
    @android.webkit.JavascriptInterface
    fun dispatcher(string: String){
        form = Gson().fromJson(string, WebViewDataForm::class.java)

        val method = this::class.java.methods.first { it.name == form!!.getFunction() }

        if(form!!.getParameters() == null){
            method.invoke(this)
        }else{
            method.invoke(this, form!!.getParameters())
        }
    }

    /**
        WebViewDataForm은 Web에서 전송된 파라미터 값들에 대해 Object화 시킨 것이다
    */
    private class WebViewDataForm {
        private var funcName : String = ""
        private var data: HashMap<String, @JvmSuppressWildcards Any>? = null

        fun getFunction() = funcName
        fun getParameters() = data
    }
}
 

주석에도 간단하게 표시를 했지만, 웹에서 호출하는 모든 메소드는 dispatcher에서 처리를 하도록 한다. 전송된 값은 JSON을 문자열로 변형시켜서 들어오기 때문에 객체로 사용할 수 있게끔 Gson 라이브러리을 이용하여 모델링 했으며, invoke를 이용하여 문자열로도 메소드를 단순 호출하거나 파라미터까지 던질 수 있도록 변형했다.

 

3. Connection!!

JavascriptInterface를 통해 Web과의 연결을 위해서는 WebView에 등록을 해줘야 한다. addJavascriptInterface는 JavascriptInterface를 등록하기 위한 메소드로, 클릭리스너와 비슷한 역할을 한다고 생각하면 되겠다. 대신에 set이 아닌 add의 형태이기 때문에 여러 인터페이스를 추가할 수 있으며, 해당 인터페이스는 Key 값을 통해 분류할 수 있게 된다.

WebView.addJavascriptInterface(object: JSInterface() {
    /* 
        정의할 메소드들을 추가한다
    */
}, JSInterface.INTF)
 

add를 시작할 때 Abstract Class에서 메소드만 정의하면 된다. 왜냐하면 호출은 dispatcher에서 알아서 하기 때문에 Android에 메소드를 정의하고 웹에서 호출을 하면 된다.

 

단, 메소드를 정의할 때에 다음과 같은 규칙(?)이 있다.

 

A. 메소드의 파라미터를 정의할 때에는 HashMap<String, @JvmSuppressWildcards Any>로 정의하도록 한다.

B. 메소드의 파라미터가 없을 경우에는 적지 않아도 된다.

 

dispatcher에서 받을 때 파라미터의 경우에는 HashMap<String, @JvmSuppressWildcards Any>로 받기로 했기 때문에 정의되는 파라미터도 똑같이 되어야 한다.

// JS 호출이 이렇다면, 
env.send('sum', { a: 1, b: 2})

// Android 정의는
WebView.addJavascriptInterface(object: JSInterface() {
    fun sum(datas: HashMap<String, @JvmSuppressWildcards Any>){
        println(${datas["a"]} + ${datas["b"]})
    }
}, JSInterface.INTF)
 

더하기 예시를 보게되면,

A. JS에서는 sum이라는 메소드에 a는 1, b는 2 를 던지도록 할 것이다.

B. Android에서는 a값과 b값을 더한다는 것을 출력하는 간단한 소스이다.

나는 여러 파라미터를 던지기 위해서 Map형식을 통해 해결하고자 했고, 전달 하고자 하는 값이 많은 경우에는 전부 나열하지 않아도 Object로 값을 넘길 수 있기 때문에 정말 편하다. 대신 값이 하나 일 때에도 똑같은 로직을 사용한다는 것이 단점일 수도 있겠다.

 

다른 방식으로도 편하게 만들거나 할 수도 있을 것 같지만 아직까지는 소스의 변형에 대해 크게 생각해보지는 않았고, 좀 더 불편하게 되면 리뉴얼 할 때에나 더 발전 시킬 것 같다.

 

일단은 이렇게도 개발 할 수 있다는 것을 남기며 이번 글을 마쳐보도록 한다

 

반응형
Comments