[Android] 웹뷰 JavascriptInterface 난독화 트러블 슈팅

강승구·2023년 8월 13일

앱 개발을 하다보면, WebView를 이용해서 웹페이지와 연동하는 화면을 만드는 경우가 종종 있다.
이때 네이티브 앱과 앱뷰간의 연동을 위해 사용하는 것이 JavascriptInterface이다.

앱 내에서 WebView를 통해 웹 페이지를 표시하고, 웹 페이지에서 발생한 이벤트나 데이터를 네이티브 코드로 전달하거나, 반대로 네이티브 코드에서 웹 페이지로 데이터를 전달하는 상황에서 JavascriptInterface를 사용한다.

만약 WebView의 버튼을 클릭했을 때 네이티브 앱의 토스트 메세지를 띄우고 싶다면 아래와 같이 네이티브에서는 인터페이스를 정의하고 웹에서는 네이티브에 정의된 메소드를 호출할 수 있다.

class WebAppInterface(private val context: Context) {

    // 자바스크립트에서 호출할 수 있는 메서드
    @JavascriptInterface
    fun showToast(message: String) {
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}
<script type="text/javascript">
    function callAndroidToast() {
        Android.showToast("Hello from JavaScript!");
    }
</script>

1. 문제 상황

당시 네이티브 앱에서 웹뷰에 있는 "알림 받기 버튼"을 클릭하면, FCM 알림을 보내는 기능을 개발하고 있었다.
이를 위해 웹뷰 버튼을 클릭하면, 앱에서는 Notification 권한 승인 여부를 확인하고, 승인이 된 경우에만 알림을 보내주기 위해 권한 승인 여부를 boolean 값으로 전달해주어야했다.

따라서 웹뷰와 브릿지 통신을 하기 위한 DTO를 정의하고, Gson 라이브러리를 통해 JSON 문자열로 변환해, Web Bridge 방식으로 데이터를 웹 서버로 전달하는 방식을 사용했다.

data class PinMoneyResult(
    val isGranted: Boolean,
    val id: String
)

fun sendPinMoneyResult(id: String, isAlarmOn: Boolean) = externalScope.launch {
    val pinMoneyResult = PinMoneyResult(isGranted = isAlarmOn, id = id)
    val json = Gson().toJson(pinMoneyResult)

    webView.evaluateJavascript("window.sendPinMoneyResult('$json');") {
        LogD(it.toString())
    }
}

debug 빌드에서는 이 과정이 문제없이 작동했지만, release 모드에서는 해당 기능이 정상적으로 동작하지 않아 웹 개발자와 같이 문제를 분석해본 결과, 웹으로 전달되는 Json 데이터가 의미 없는 값으로 들어가고 있음을 알게되었다.


2. 원인 분석

문제의 원인을 찾기 위해, debug 모드에서는 정상적으로 작동하고, release 모드에서만 문제가 발생한다는 점에 초점을 두고 분석해보았다.

1. 서버 차이로 인한 문제 추정
우선, 당시 개발하던 앱은 아래와 같이 debug 모드에서 호출하는 서버와, release 모드에서 호출하는 서버가 분리 되어있었다.

val hostUrl = if (BuildConfig.DEBUG) "dev-api.*****" else "api.*****"

따라서 debug 서버와 release 서버에서 동일한 API 호출에 대한 응답을 비교하기 위해 각 서버의 응답 데이터를 확인하고, JSON 형식이나 응답 구조가 차이가 있는지 로그로 분석해보았다.
하지만 두 서버에서 반환되는 데이터는 동일했고, 서버 간 차이로 인한 문제가 아니라는 것을 확인했다.

2. 난독화 여부
debug 모드와 release 모드의 차이점이 어떤게 있을까 고민하다 난독화가 떠올랐다.
당시 개발하던 앱에서, release 모드로 빌드를 하는 경우에는, isMinifyEnabled를 true로 설정하여 난독화 옵션을 활성화해주고 있었다.

해당 설정에 따라 release 빌드에서는 난독화가 적용되었고, debug 빌드에서는 난독화가 적용되지 않았기 때문에 두 빌드 모드 간의 동작 차이가 발생한 것으로 추측했다.

따라서 해당 옵션을 fasle로 수정한 뒤 release 모드로 빌드하고 해당 기능을 테스트 해보니 정상적으로 동작하는 것을 확인하였다.
즉, DTO가 Proguard에 의해 난독화 되며 내부의 프로퍼티 명들이 의미 없는 값들로 변경되었고, 이로 인해 웹에는 정상적인 데이터가 전달되지 않고 있던 것이 원인이었다.


3. 해결 과정

구글링을 통해 많은 사람들이 나와 같은 문제를 겪었다는 것을 알게되었고 이를 참고해 다음과 같은 방법들을 적용해 문제를 해결하였다.

1. JavaInterface 관련 proguard 파일 생성
우선 JavascriptInterface가 포함된 메서드가 난독화되지 않도록 ProGuard 설정 파일에 예외 규칙을 추가해주어 JavaScript와 네이티브 안드로이드 코드 간의 통신에서 메서드 이름이 달라지는 것을 방지해주었다.

-keepattributes JavascriptInterface
-keepattributes *Annotation*

-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

-keepclassmembers class com.company.app.jsBridge.BaseJavascriptInterface {
    public *;
}

-keep public class com.company.app.jsBridge.BaseJavascriptInterface

2. Gson 관련 proguard 파일 생성
Gson을 사용하여 JSON 문자열을 자바 객체로 변환하거나 그 반대로 변환할 때, 난독화로 인해 필드 이름이 변경되지 않도록 Gson 사용에 필요한 ProGuard 규칙을 별도로 설정했다.

##---------------Begin: proguard configuration for Gson  ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature


# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }

# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { *; }

##---------------End: proguard configuration for Gson  ----------

3. @Keep 어노테이션 추가
@Keep 어노테이션은 개별적인 클래스, 메서드, 필드에 적용하여 그 요소가 난독화되지 않도록 명시하는 방법이다. 따라서 해당 어노테이션을 DTO에 추가해 ProGuard가 해당 코드를 유지하도록했다.

@Keep
data class PinMoneyResult(
    val isGranted: Boolean,
    val id: String
)
profile
강승구

0개의 댓글