[Android] 자바스크립트 인터페이스
자바스크립트 인터페이스란?
@JavascriptInterface는 안드로이드 WebView 내에서 실행되는 자바스크립트 코드가 네이티브 자바/코틀린 코드를 호출할 수 있도록 하는 브리지 메커니즘입니다. 이는 마치 서로 다른 언어를 사용하는 두 세계 사이에 통역사를 배치하는 것과 같습니다.
1. 기본 작동 원리
자바스크립트 인터페이스는 본질적으로 두 개의 완전히 다른 실행 환경을 연결합니다:
- 자바/코틀린 환경: 안드로이드 네이티브 코드가 실행되는 JVM(Java Virtual Machine) 또는 ART(Android Runtime)
- 자바스크립트 환경: WebView 내부의 자바스크립트 엔진(V8 또는 WebKit)이 코드를 실행하는 공간
이 두 환경은 원래 다음과 같은 이유로 완전히 격리되어 있습니다:
- 서로 다른 프로그래밍 언어를 사용
- 각각 다른 가상 머신에서 실행
- 별도의 메모리 공간을 사용
- 다른 스레드 모델과 실행 컨텍스트를 가짐
@JavascriptInterface는 이러한 장벽을 허물고, 두 환경 사이에 안전하고 제어된 통신 채널을 구축합니다.
2. 내부 동작 메커니즘
2.1 바인딩 과정 (Binding Process)
webView.addJavascriptInterface(webBridge, "android")
이 한 줄의 코드가 실행될 때 내부적으로는 다음과 같은 복잡한 과정이 진행됩니다:
- 객체 등록: webBridge 객체가 WebView의 자바스크립트 엔진에 등록됩니다.
- 프록시 생성: 자바스크립트 엔진은 이 자바/코틀린 객체에 대응하는 자바스크립트 프록시 객체를 생성합니다.
- 글로벌 네임스페이스 주입: 생성된 프록시 객체가 웹페이지의 전역 window 객체에 android라는 이름으로 주입됩니다.
2.2 메소드 노출 과정
@JavascriptInterface
fun getCurrentLocation() {
// 위치 정보를 가져오는 네이티브 코드
}
여기서 중요한 점은 @JavascriptInterface 어노테이션이 달린 메소드만이 자바스크립트에 노출된다는 것입니다. 이 과정에서 다음과 같은 일이 발생합니다:
- 리플렉션 사용: 안드로이드 시스템은 자바의 리플렉션(Reflection) API를 사용하여 등록된 객체에서 @JavascriptInterface 어노테이션이 달린 모든 public 메소드를 식별합니다.
- 메소드 매핑: 식별된 메소드들을 자바스크립트에서 호출 가능한 함수로 매핑합니다.
- 보안 검증: 이 과정에서 안드로이드는 보안 검증을 수행하여 악의적인 접근을 방지합니다.
3. 양방향 통신의 세부 과정
3.1 웹 → 네이티브 호출 과정
웹 페이지에서 다음과 같이 호출하면:
window.android.getCurrentLocation();
내부적으로 다음과 같은 일이 발생합니다:
- 호출 감지: 자바스크립트 엔진이 android 객체의 getCurrentLocation 메소드 호출을 감지합니다.
- 네이티브 브릿지 활성화: WebView는 이 호출을 감지하고 특별한 네이티브 브릿지를 통해 JNI(Java Native Interface)를 활성화합니다.
- 메소드 검색: 안드로이드 시스템은 등록된 객체에서 getCurrentLocation 이름의 메소드를 찾습니다.
- 스레드 전환: 자바스크립트는 UI 스레드에서 실행되므로, 호출도 UI 스레드에서 발생합니다. 그러나 장시간 실행되는 작업은 다른 스레드로 전환해야 합니다.
private val handler = Handler(Looper.getMainLooper())@JavascriptInterfacefun getCurrentLocation() { handler.post { // UI 스레드를 차단하지 않는 비동기 처리 requestLocation() }}
- 메소드 실행: 찾은 메소드가 실행되어 네이티브 기능(위치 정보 획득)이 작동합니다.
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
)
}
이 과정은 다음과 같이 작동합니다:
- 자바스크립트 코드 생성: 네이티브 코드가 실행할 자바스크립트 코드 문자열을 동적으로 생성합니다.
- 자바스크립트 엔진 접근: evaluateJavascript 메소드를 호출하여 WebView의 자바스크립트 엔진에 접근합니다.
- 코드 주입 및 실행: 생성된 코드가 웹페이지의 컨텍스트에 주입되고 실행됩니다.
- 웹 함수 호출: 웹페이지에 미리 정의된 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. 보안 메커니즘
자바스크립트 인터페이스는 강력한 기능인 만큼 보안 위험도 존재합니다. 안드로이드는 이를 위해 다음과 같은 보안 메커니즘을 구현합니다:
- 명시적 노출: @JavascriptInterface 어노테이션이 있는 메소드만 웹에 노출됩니다.
- 안드로이드 버전 보호: 안드로이드 4.2(API 17) 이상에서는 이 어노테이션이 없는 메소드는 자바스크립트에서 절대 접근할 수 없습니다. 이는 이전 버전에서 발생했던 심각한 보안 취약점을 해결하기 위함입니다.
- 컨텍스트 분리: 웹 코드는 제한된 인터페이스를 통해서만 네이티브에 접근할 수 있어, 무제한적 접근이 허용되지 않습니다.
- UI 스레드 보호: 장기 실행 작업을 별도 스레드로 전환하여 UI 프리징을 방지합니다.
6. 기술적 구현 세부사항
이 기능이 작동하는 하위 수준의 기술적 세부 사항을 살펴보면:
- V8/WebKit 브릿지: 안드로이드의 WebView는 Chrome의 V8 자바스크립트 엔진이나 WebKit 기반 엔진을 사용합니다.
- JNI 레이어: 자바/코틀린과 C++로 작성된 자바스크립트 엔진 사이에는 JNI(Java Native Interface) 레이어가 있습니다.
- 객체 참조 관리: 자바스크립트에 노출된 객체에 대한 참조는 가비지 컬렉션 방지를 위해 특별히 관리됩니다.
- 마샬링/언마샬링: 데이터는 두 환경 사이를 오갈 때 자동으로 직렬화/역직렬화됩니다.
7. 제한사항과 최적화 기법
이 기술을 사용할 때 다음과 같은 제한사항과 최적화 기법을 알아두면 좋습니다:
7.1 제한사항
- 직렬화 오버헤드: 복잡한 객체는 직렬화/역직렬화 과정에서 성능 비용이 발생합니다.
- 동기 실행 제한: UI 스레드 차단을 방지하기 위해 복잡한 작업은 별도 스레드로 처리해야 합니다.
- 디버깅 어려움: 두 환경 사이의 오류를 디버깅하는 것은 간단하지 않습니다.
7.2 최적화 기법
- 최소한의 데이터 전송: 필요한 데이터만 주고받아 직렬화 비용을 최소화합니다.
- 배치 처리: 여러 작은 호출보다 큰 배치로 처리하는 것이 효율적입니다.
- 자주 사용하는 참조 캐싱: 자주 사용하는 객체 참조를 캐싱하여 검색 비용을 줄입니다.
- 비동기 패턴 활용: 모든 장기 실행 작업은 비동기로 처리합니다.
// 나쁜 예: 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 스레드 차단을 방지하고 사용자 경험 향상
- 복잡하거나 시간이 오래 걸리는 작업에 적합