Android

[Android] 자바스크립트 인터페이스

김한토 2025. 5. 8. 13:29
반응형

 

자바스크립트 인터페이스란?

@JavascriptInterface는 안드로이드 WebView 내에서 실행되는 자바스크립트 코드가 네이티브 자바/코틀린 코드를 호출할 수 있도록 하는 브리지 메커니즘입니다. 이는 마치 서로 다른 언어를 사용하는 두 세계 사이에 통역사를 배치하는 것과 같습니다.

1. 기본 작동 원리

자바스크립트 인터페이스는 본질적으로 두 개의 완전히 다른 실행 환경을 연결합니다:

  • 자바/코틀린 환경: 안드로이드 네이티브 코드가 실행되는 JVM(Java Virtual Machine) 또는 ART(Android Runtime)
  • 자바스크립트 환경: WebView 내부의 자바스크립트 엔진(V8 또는 WebKit)이 코드를 실행하는 공간

이 두 환경은 원래 다음과 같은 이유로 완전히 격리되어 있습니다:

  • 서로 다른 프로그래밍 언어를 사용
  • 각각 다른 가상 머신에서 실행
  • 별도의 메모리 공간을 사용
  • 다른 스레드 모델과 실행 컨텍스트를 가짐

@JavascriptInterface는 이러한 장벽을 허물고, 두 환경 사이에 안전하고 제어된 통신 채널을 구축합니다.

2. 내부 동작 메커니즘

2.1 바인딩 과정 (Binding Process)

webView.addJavascriptInterface(webBridge, "android")

이 한 줄의 코드가 실행될 때 내부적으로는 다음과 같은 복잡한 과정이 진행됩니다:

  1. 객체 등록: webBridge 객체가 WebView의 자바스크립트 엔진에 등록됩니다.
  2. 프록시 생성: 자바스크립트 엔진은 이 자바/코틀린 객체에 대응하는 자바스크립트 프록시 객체를 생성합니다.
  3. 글로벌 네임스페이스 주입: 생성된 프록시 객체가 웹페이지의 전역 window 객체에 android라는 이름으로 주입됩니다.

2.2 메소드 노출 과정

@JavascriptInterface
fun getCurrentLocation() {
    // 위치 정보를 가져오는 네이티브 코드
}

여기서 중요한 점은 @JavascriptInterface 어노테이션이 달린 메소드만이 자바스크립트에 노출된다는 것입니다. 이 과정에서 다음과 같은 일이 발생합니다:

  1. 리플렉션 사용: 안드로이드 시스템은 자바의 리플렉션(Reflection) API를 사용하여 등록된 객체에서 @JavascriptInterface 어노테이션이 달린 모든 public 메소드를 식별합니다.
  2. 메소드 매핑: 식별된 메소드들을 자바스크립트에서 호출 가능한 함수로 매핑합니다.
  3. 보안 검증: 이 과정에서 안드로이드는 보안 검증을 수행하여 악의적인 접근을 방지합니다.

3. 양방향 통신의 세부 과정

3.1 웹 → 네이티브 호출 과정

웹 페이지에서 다음과 같이 호출하면:

window.android.getCurrentLocation();

내부적으로 다음과 같은 일이 발생합니다:

  1. 호출 감지: 자바스크립트 엔진이 android 객체의 getCurrentLocation 메소드 호출을 감지합니다.
  2. 네이티브 브릿지 활성화: WebView는 이 호출을 감지하고 특별한 네이티브 브릿지를 통해 JNI(Java Native Interface)를 활성화합니다.
  3. 메소드 검색: 안드로이드 시스템은 등록된 객체에서 getCurrentLocation 이름의 메소드를 찾습니다.
  4. 스레드 전환: 자바스크립트는 UI 스레드에서 실행되므로, 호출도 UI 스레드에서 발생합니다. 그러나 장시간 실행되는 작업은 다른 스레드로 전환해야 합니다.
    private val handler = Handler(Looper.getMainLooper())@JavascriptInterfacefun getCurrentLocation() {    handler.post {        // UI 스레드를 차단하지 않는 비동기 처리        requestLocation()    }}
    
  5. 메소드 실행: 찾은 메소드가 실행되어 네이티브 기능(위치 정보 획득)이 작동합니다.

 

3.2 매개변수 및 반환 값 처리

매개변수와 반환 값은 다음과 같이 처리됩니다:

  • 매개변수 직렬화: 자바스크립트에서 전달된 매개변수는 자바/코틀린 타입으로 자동 변환됩니다.
    • 문자열, 숫자, 불리언 → String, int/float, boolean
    • JSON 객체 → JSONObject 또는 사용자 정의 객체
  • 반환 값 처리:
    @JavascriptInterfacefun add(a: Int, b: Int): Int {    return a + b}
    
    • 단순 메소드 호출은 위와 같이 동기적으로 반환 값을 즉시 전달할 수 있습니다.
    • 복잡한 비동기 작업은 콜백 패턴이 필요합니다.

3.3 네이티브 → 웹 통신 (콜백 패턴)

위치 정보와 같은 비동기 작업의 결과를 웹으로 전달하려면:

private fun sendLocationToWeb(latitude: Double, longitude: Double) {
    webView.evaluateJavascript(
        "javascript:window.receiveLocation($latitude, $longitude)",
        null
    )
}

이 과정은 다음과 같이 작동합니다:

  1. 자바스크립트 코드 생성: 네이티브 코드가 실행할 자바스크립트 코드 문자열을 동적으로 생성합니다.
  2. 자바스크립트 엔진 접근: evaluateJavascript 메소드를 호출하여 WebView의 자바스크립트 엔진에 접근합니다.
  3. 코드 주입 및 실행: 생성된 코드가 웹페이지의 컨텍스트에 주입되고 실행됩니다.
  4. 웹 함수 호출: 웹페이지에 미리 정의된 window.receiveLocation 함수가 호출됩니다.

이 방식을 통해 양방향 통신이 완성됩니다:

  • 웹 → 네이티브: @JavascriptInterface로 노출된 메소드 호출
  • 네이티브 → 웹: evaluateJavascript()를 통한 웹 함수 호출

4. 실제 사례: 위치 정보 서비스 구현

다음은 실제 위치 정보 서비스를 구현한 예시 코드입니다:

4.1 네이티브 코드 (WebAppInterface.kt)

class WebAppInterface(
    private val context: Context,
    private val webView: WebView
) {
    private val TAG = "WebAppInterface"
    private val handler = Handler(Looper.getMainLooper())
    private var locationListener: LocationListener? = null

    @JavascriptInterface
    fun getCurrentLocation() {
        Log.d(TAG, "getCurrentLocation 호출됨")

        handler.post {
            try {
                requestLocation()
            } catch (e: Exception) {
                Log.e(TAG, "위치 정보 가져오기 오류", e)
                sendLocationError("위치 정보를 가져오는 중 오류가 발생했습니다: ${e.message}")
            }
        }
    }

    private fun requestLocation() {
        val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        
        // 위치 권한 확인 및 요청 로직 생략...
        
        // 위치 업데이트 요청
        locationListener = object : LocationListener {
            override fun onLocationChanged(location: Location) {
                sendLocationToWeb(location.latitude, location.longitude)
                cleanupLocationResources() // 위치 받은 후 리소스 정리
            }
            // 다른 리스너 메소드 생략...
        }
        
        // 위치 요청 로직...
    }

    private fun sendLocationToWeb(latitude: Double, longitude: Double) {
        Log.d(TAG, "위치 정보 전송: $latitude, $longitude")

        try {
            webView.evaluateJavascript(
                "javascript:window.receiveLocation($latitude, $longitude)",
                null
            )
        } catch (e: Exception) {
            Log.e(TAG, "위치 정보 전송 오류", e)
        }
    }
}

4.2 웹 코드 (JavaScript)

// 웹뷰에 함수 정의
window.receiveLocation = function(latitude, longitude) {
    console.log("위치 정보 수신: " + latitude + ", " + longitude);
    
    // 위치 정보로 지도에 마커 표시 등의 작업 수행
    updateMapPosition(latitude, longitude);
    
    // UI 업데이트
    document.getElementById('location-status').textContent = 
        '현재 위치: ' + latitude.toFixed(6) + ', ' + longitude.toFixed(6);
};

// 네이티브 기능 호출 버튼에 이벤트 연결
document.getElementById('get-location-btn').addEventListener('click', function() {
    // 네이티브 메소드 호출
    window.android.getCurrentLocation();
});

4.3 WebView 설정 (Fragment/Activity)

@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
private fun initWebView() {
    webView.settings.apply {
        javaScriptEnabled = true  // JavaScript 활성화 - 필수!
        domStorageEnabled = true  // localStorage 등 웹 스토리지 지원
    }

    // WebAppInterface 초기화 및 등록
    val webAppInterface = WebAppInterface(requireContext(), webView)
    webView.addJavascriptInterface(webAppInterface, "android")

    webView.loadUrl("https://example.com/map-page")
}

5. 보안 메커니즘

자바스크립트 인터페이스는 강력한 기능인 만큼 보안 위험도 존재합니다. 안드로이드는 이를 위해 다음과 같은 보안 메커니즘을 구현합니다:

  1. 명시적 노출: @JavascriptInterface 어노테이션이 있는 메소드만 웹에 노출됩니다.
  2. 안드로이드 버전 보호: 안드로이드 4.2(API 17) 이상에서는 이 어노테이션이 없는 메소드는 자바스크립트에서 절대 접근할 수 없습니다. 이는 이전 버전에서 발생했던 심각한 보안 취약점을 해결하기 위함입니다.
  3. 컨텍스트 분리: 웹 코드는 제한된 인터페이스를 통해서만 네이티브에 접근할 수 있어, 무제한적 접근이 허용되지 않습니다.
  4. UI 스레드 보호: 장기 실행 작업을 별도 스레드로 전환하여 UI 프리징을 방지합니다.

6. 기술적 구현 세부사항

이 기능이 작동하는 하위 수준의 기술적 세부 사항을 살펴보면:

  1. V8/WebKit 브릿지: 안드로이드의 WebView는 Chrome의 V8 자바스크립트 엔진이나 WebKit 기반 엔진을 사용합니다.
  2. JNI 레이어: 자바/코틀린과 C++로 작성된 자바스크립트 엔진 사이에는 JNI(Java Native Interface) 레이어가 있습니다.
  3. 객체 참조 관리: 자바스크립트에 노출된 객체에 대한 참조는 가비지 컬렉션 방지를 위해 특별히 관리됩니다.
  4. 마샬링/언마샬링: 데이터는 두 환경 사이를 오갈 때 자동으로 직렬화/역직렬화됩니다.

7. 제한사항과 최적화 기법

이 기술을 사용할 때 다음과 같은 제한사항과 최적화 기법을 알아두면 좋습니다:

7.1 제한사항

  1. 직렬화 오버헤드: 복잡한 객체는 직렬화/역직렬화 과정에서 성능 비용이 발생합니다.
  2. 동기 실행 제한: UI 스레드 차단을 방지하기 위해 복잡한 작업은 별도 스레드로 처리해야 합니다.
  3. 디버깅 어려움: 두 환경 사이의 오류를 디버깅하는 것은 간단하지 않습니다.

7.2 최적화 기법

  1. 최소한의 데이터 전송: 필요한 데이터만 주고받아 직렬화 비용을 최소화합니다.
  2. 배치 처리: 여러 작은 호출보다 큰 배치로 처리하는 것이 효율적입니다.
  3. 자주 사용하는 참조 캐싱: 자주 사용하는 객체 참조를 캐싱하여 검색 비용을 줄입니다.
  4. 비동기 패턴 활용: 모든 장기 실행 작업은 비동기로 처리합니다.
// 나쁜 예: UI 스레드 차단
@JavascriptInterface
fun processLargeData(jsonData: String): String {
    return heavyProcessing(jsonData)  // UI 스레드 차단!
}

// 좋은 예: 비동기 처리
@JavascriptInterface
fun processLargeData(jsonData: String) {
    Thread {
        val result = heavyProcessing(jsonData)
        handler.post {
            webView.evaluateJavascript(
                "javascript:window.receiveResult('$result')",
                null
            )
        }
    }.start()
}

8. 실무 적용 팁

8.1 오류 처리와 폴백 전략

네트워크 오류, 위치 서비스 비활성화 등의 상황에 대비하여 항상 폴백 전략을 구현하는 것이 좋습니다:

private fun sendLocationError(errorMessage: String) {
    Log.e(TAG, "위치 오류: $errorMessage")
    
    try {
        // 오류 알림
        webView.evaluateJavascript(
            "javascript:window.locationError('${errorMessage.replace("'", "\\'")}')",
            null
        )
        
        // 기본 위치 제공 (서울시청 좌표)
        sendLocationToWeb(37.566535, 126.9779692)
    } catch (e: Exception) {
        Log.e(TAG, "위치 오류 전송 실패", e)
    }
}

8.2 리소스 관리

파일 다운로드, 위치 서비스 등 시스템 리소스를 사용할 때는 적절한 해제 코드를 구현해야 합니다:

fun cleanupLocationResources() {
    try {
        locationListener?.let {
            val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
            locationManager?.removeUpdates(it)
            locationListener = null
        }
    } catch (e: Exception) {
        Log.e(TAG, "위치 리소스 정리 중 오류", e)
    }
}


+++

 

 

  • 직접 반환값 방식:
    • 단순하고 즉시 결과를 반환할 수 있는 작업에 적합
    • 자바스크립트에서 동기적으로 결과를 받음
    • UI 스레드를 차단할 수 있어 간단한 작업에만 사용해야 함
  • evaluateJavascript(콜백) 방식:
    • 비동기 작업에 필수적
    • 네이티브에서 자바스크립트로 결과를 "푸시"하는 개념
    • UI 스레드 차단을 방지하고 사용자 경험 향상
    • 복잡하거나 시간이 오래 걸리는 작업에 적합

 

참고 문서

반응형