이 페이지에서는 WebView의 웹 콘텐츠와 호스트 Android 애플리케이션 간의 통신을 용이하게 하기 위해 JavaScript 브리지라고도 하는 네이티브 브리지를 설정하는 다양한 방법과 권장사항을 설명합니다.
이를 통해 웹 개발자는 JavaScript를 사용하여 표준 웹 API가 일반적으로 제공하지 않는 카메라, 파일 시스템, 고급 하드웨어 센서와 같은 네이티브 플랫폼 기능에 액세스할 수 있습니다.
사용 사례
JavaScript 브리지 구현을 사용하면 웹 콘텐츠가 Android 운영체제에 더 깊이 액세스해야 하는 다양한 통합 시나리오가 가능합니다. 다음은 몇 가지 예입니다.
- 플랫폼 통합: 웹페이지에서 네이티브 Android UI 구성요소 (예: 생체 인식 프롬프트,
BottomSheetDialog)를 트리거합니다. - 성능: 무거운 계산 작업을 네이티브 Java 또는 Kotlin 코드로 오프로드합니다.
- 데이터 지속성: 로컬 암호화 데이터베이스 또는 공유 환경설정에 액세스합니다.
- 대량 데이터 전송: 앱과 웹 렌더러 간에 미디어 파일 또는 복잡한 데이터 구조를 전달합니다.
커뮤니케이션 메커니즘
Android는 네이티브 브리지를 설정하기 위해 세 가지 기본 세대의 API를 제공합니다. 이러한 방법은 모두 사용할 수 있지만 보안, 사용성, 성능이 크게 다릅니다.
addWebMessageListener 사용 (권장)
addWebMessageListener는 웹 콘텐츠와 네이티브 앱 코드 간 통신에 가장 최신이며 권장되는 접근 방식입니다. JavaScript 인터페이스의 사용 편의성과 메시지 시스템의 보안을 결합합니다.
작동 방식: 앱은 특정 이름과 허용된 출처 규칙 집합을 사용하여 리스너를 추가합니다. 그러면 WebView는 페이지가 로드되기 시작하는 순간부터 JavaScript 객체가 전역 범위 (window.objectName)에 있는지 확인합니다.
초기화: WebView가 스크립트가 실행되기 전에 JavaScript 객체를 삽입하도록 하려면 loadUrl()를 호출하기 전에 addWebMessageListener를 호출해야 합니다.
주요 기능
보안 및 신뢰: 기존 API와 달리 이 메서드는 초기화 중에
allowedOriginRules의Set<String>이 필요합니다. 이는 신뢰를 구축하는 기본 메커니즘입니다.https://example.com와 같은 신뢰할 수 있는 출처를 지정하면 WebView는 삽입된 JavaScript 객체를 정확한 출처에서 로드된 웹페이지에만 노출하도록 보장합니다.네이티브 리스너 콜백은 모든 메시지와 함께
sourceOrigin매개변수를 수신합니다. 브리지에서 허용된 여러 출처를 지원하는 경우 이를 사용하여 발신자의 정확한 출처를 확인할 수 있습니다.WebView는 플랫폼 수준에서 이러한 출처 확인을 엄격하게 적용하므로 앱은 일반적으로 신뢰할 수 있는
sourceOrigin에서 수신한 메시지를 사실로 간주할 수 있어 대부분의 표준 구현에서 엄격한 페이로드 유효성 검사가 필요하지 않습니다.- WebView는 스키마 (HTTP/HTTPS), 호스트, 포트에 대해 규칙을 일치시킵니다.
- WebView는 경로를 무시합니다. 예를 들어
https://example.com은https://example.com/login및https://example.com/home을 허용합니다. - WebView는 하위 도메인의 호스트 시작 부분으로 와일드 카드를 엄격하게 제한합니다. 예를 들어
https://*.example.com는https://foo.example.com와 일치하지만https://example.com와는 일치하지 않습니다.https://example.com와 하위 도메인을 모두 일치시켜야 하는 경우 각 출처 규칙을 허용 목록에 별도로 추가해야 합니다 (예:"https://example.com", "https://*.example.com"). 스킴이나 도메인 중간에 와일드 카드를 사용할 수는 없습니다.
이렇게 하면 브리지가 확인된 도메인으로 제한되어 승인되지 않은 서드 파티 콘텐츠나 삽입된 iframe이 네이티브 코드를 실행하지 못합니다.
다중 프레임 지원: 출처 규칙과 일치하는 모든 프레임에서 작동합니다.
스레딩: 리스너 콜백은 애플리케이션의 기본 (UI) 스레드에서 실행됩니다. 브리지에서 복잡한 데이터 처리, JSON 파싱 또는 데이터베이스 조회를 처리해야 하는 경우 '앱이 응답하지 않음'(ANR) 오류로 애플리케이션 UI가 정지되지 않도록 백그라운드 스레드로 작업을 오프로드해야 합니다.
양방향: 웹페이지가 메시지를 보내면 앱은 특정 프레임에 다시 메시지를 보내는 데 사용할 수 있는
JavaScriptReplyProxy를 수신합니다. 이replyProxy객체를 유지하고 언제든지 사용하여 페이지가 전송하는 각 개별 메시지에 답장하는 것뿐만 아니라 페이지에 원하는 수의 메시지를 전송할 수 있습니다. 원래 프레임이 다른 곳으로 이동하거나 소멸되면 프록시에서postMessage()를 사용하여 전송된 메시지는 자동으로 무시됩니다.앱 측 시작: 웹페이지는 항상 앱과의 통신 채널을 시작해야 하지만 네이티브 앱은 웹페이지에 이 프로세스를 시작하라고 일방적으로 프롬프트할 수 있습니다. 네이티브 앱은
addDocumentStartJavaScript()(페이지가 로드되기 전에 JavaScript를 평가) 또는evaluateJavaScript()(페이지가 로드된 후 JavaScript를 평가)를 사용하여 웹페이지와 통신할 수 있습니다.
제한사항: 이 API는 데이터를 문자열 또는 byte[] 배열로 전송합니다. JSON 객체와 같은 더 복잡한 데이터 구조의 경우 이러한 형식 중 하나로 직렬화한 다음 다른 쪽에서 역직렬화하여 데이터 구조를 재구성해야 합니다.
사용 예:
양방향 메시지 교환의 전체 시퀀스를 이해하려면 이벤트가 다음 순서로 진행됩니다.
- 시작 (앱): 네이티브 앱이
addWebMessageListener로 리스너를 등록하고loadUrl()로 웹페이지를 로드합니다. - 메시지 전송 (웹): 웹페이지의 JavaScript가
myObject.postMessage(message)를 호출하여 통신을 시작합니다. - 메시지 수신 및 답장 (앱): 앱은 리스너 콜백에서 메시지를 수신하고 제공된
replyProxy.postMessage()를 사용하여 답장합니다. - 답장 수신 (웹): 웹페이지는
myObject.onmessage()콜백 함수에서 비동기 답장을 수신합니다.
Kotlin
val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
// Handle the message from JS
replyProxy.postMessage("Acknowledged!")
}
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val allowedOrigins = setOf("https://www.example.com")
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}
자바
WebMessageListener myListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Handle the message from JS
replyProxy.postMessage("Acknowledged!");
};
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
Set<String> allowedOrigins = Set.of("https://www.example.com");
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener);
}
다음 JavaScript는 웹 콘텐츠가 네이티브 앱에서 메시지를 수신하고 myObject 프록시를 통해 자체 메시지를 전송할 수 있도록 하는 addWebMessageListener의 클라이언트 측 구현을 보여줍니다.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
postWebMessage 사용 (대체)
Android는 웹의 window.postMessage와 유사한 비동기 메시지 기반 대안을 제공하기 위해 이를 도입했습니다.
작동 방식: 앱은 WebViewCompat.postWebMessage를 사용하여 웹페이지의 기본 프레임에 페이로드를 전송합니다. 양방향 통신 채널을 설정하려면 WebMessageChannel를 만들고 메시지와 함께 포트 중 하나를 웹 콘텐츠에 전달하면 됩니다.
특성:
- 비동기:
addWebMessageListener와 마찬가지로 이 메서드는 비동기 메시지를 사용하므로 앱이 백그라운드에서 데이터를 처리하는 동안 웹페이지가 사용자 상호작용에 응답할 수 있습니다. - 출처 인식:
targetOrigin를 지정하여 WebView가 신뢰할 수 있는 웹사이트에만 데이터를 전송하도록 할 수 있습니다.
제한사항:
- 범위: 이 API는 통신을 기본 프레임으로 제한합니다. iframe에 직접 주소를 지정하거나 메시지를 보내는 것은 지원하지 않습니다.
- URI 제한: 타겟 출처로 '*'를 지정하지 않는 한
data:URI,file:URI 또는loadData()를 사용하여 로드된 콘텐츠에는 이 메서드를 사용할 수 없습니다. 이렇게 하면 모든 페이지에서 메시지를 수신할 수 있습니다. - ID 위험: 웹 콘텐츠에서 발신자의 ID를 확인할 수 있는 명확한 방법이 없습니다. 웹페이지가 수신하는 메시지는 네이티브 앱이나 다른 iframe에서 시작되었을 수 있습니다.
addWebMessageListener를 지원하지 않는 이전 Android 버전에서 문자열 기반 데이터에 간단한 비동기 채널이 필요한 경우 이 메서드를 사용하세요.
addJavascriptInterface (기존) 사용
가장 오래된 방법은 네이티브 객체 인스턴스를 WebView에 직접 삽입하는 것입니다.
작동 방식: Kotlin 또는 Java 클래스를 정의하고 허용된 메서드에 @JavascriptInterface 주석을 달고 addJavascriptInterface(Object, String)를 사용하여 클래스의 인스턴스를 WebView에 추가합니다.
특성:
- 동기: Android 코드의 메서드가 반환될 때까지 JavaScript 실행 환경이 차단됩니다.
- 스레드 안전: 시스템은 백그라운드 스레드에서 메서드를 호출하므로 Kotlin 또는 Java 측에서 신중한 동기화가 필요합니다.
- 보안 위험: 기본적으로
addJavascriptInterface는 iframe을 포함하여 WebView 내의 모든 프레임에서 사용할 수 있습니다. 출처 기반 액세스 제어가 없습니다. WebView의 비동기 동작으로 인해 인터페이스를 호출하는 프레임의 URL을 안전하게 확인할 수 없습니다.WebView.getUrl()과 같은 메서드는 정확성이 보장되지 않고 요청을 한 특정 프레임을 나타내지 않으므로 보안 확인에 사용하면 안 됩니다.
메커니즘 요약
다음 표에서는 세 가지 기본 네이티브 브리지 구현 메커니즘을 간단히 비교합니다.
| 메서드 | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| 구현 | 비동기 (기본 스레드의 리스너) | 비동기 | 동기 |
| 보안 | 최고 (허용 목록 기반) | 높음 (출처 인식) | 낮음 (출처 검사 없음) |
| 복잡성 | 보통 | 보통 | 단순 |
| 방향 | 양방향 | 양방향 | 웹에서 앱으로 |
| 최소 WebView 버전 | 버전 82 (및 Jetpack Webkit 1.3.0) | 버전 45 (및 Jetpack Webkit 1.1.0) | 모든 버전 |
| 추천 | 예 | 아니요 | 아니요 |
대규모 데이터 전송 처리
수 메가바이트 문자열이나 바이너리 파일과 같은 대형 페이로드를 전송할 때는 32비트 기기에서 애플리케이션 응답 없음(ANR) 오류나 비정상 종료가 발생하지 않도록 메모리를 신중하게 관리해야 합니다. 이 섹션에서는 호스트 애플리케이션과 웹 콘텐츠 간에 상당한 양의 데이터를 전송하는 데 관련된 다양한 기술과 제한사항을 설명합니다.
바이트 배열로 바이너리 데이터 전송
WebMessageCompat 클래스를 사용하면 바이너리 데이터를 Base64 문자열로 직렬화하는 대신 byte[] 배열을 직접 보낼 수 있습니다. Base64는 데이터 크기에 약 33% 의 오버헤드를 추가하므로 이 방법이 훨씬 더 메모리 효율적이고 빠릅니다.
- 바이너리 이점: 네이티브 앱과 웹 콘텐츠 간에 이미지 파일이나 오디오와 같은 바이너리 데이터를 전송합니다.
- 제한사항: 바이트 배열을 사용하는 경우에도 시스템은 앱과 WebView가 웹 콘텐츠를 렌더링하는 데 사용하는 격리된 프로세스 간의 프로세스 간 통신 (IPC) 경계에서 데이터를 복사합니다. 이 경우에도 매우 큰 파일의 경우 상당한 메모리를 소비합니다.
다음 코드 예에서는 네이티브 앱 측에서 addWebMessageListener를 설정하여 WebMessageCompat.TYPE_ARRAY_BUFFER로 표시된 메시지를 수신하고 WebViewFeature.MESSAGE_ARRAY_BUFFER를 확인하여 선택적으로 바이너리 데이터로 응답하는 방법을 보여줍니다.
Kotlin
fun setupWebView(webView: WebView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val listener = WebViewCompat.WebMessageListener { view, message, sourceOrigin, isMainFrame, replyProxy ->
// Check if the received message is an ArrayBuffer
if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
val binaryData: ByteArray = message.arrayBuffer
// Process your binary data (image, audio, etc.)
println("Received bytes: ${binaryData.size}")
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
val replyBytes = byteArrayOf(0x01, 0x02, 0x03)
replyProxy.postMessage(replyBytes)
}
}
}
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
setOf("https://example.com"), // Security: restrict origins
listener
)
}
}
자바
public void setupWebView(WebView webView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.WebMessageListener listener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Check if the received message is an ArrayBuffer
if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
byte[] binaryData = message.getArrayBuffer();
// Process your binary data (image, audio, etc.)
System.out.println("Received bytes: " + binaryData.length);
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
byte[] replyBytes = new byte[]{0x01, 0x02, 0x03};
replyProxy.postMessage(replyBytes);
}
}
};
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
Set.of("https://example.com"), // Security: restrict origins
listener
);
}
}
다음 JavaScript 코드는 addWebMessageListener의 클라이언트 측 구현을 보여주며, 이를 통해 웹 콘텐츠는 이전 예에 삽입된 window.myBridge 프록시를 사용하여 네이티브 앱과 바이너리 데이터 (ArrayBuffer)를 주고받을 수 있습니다.
// Function to send an image or binary buffer to the app
async function sendBinaryToApp() {
const response = await fetch('image.jpg');
const buffer = await response.arrayBuffer();
// Check if the injected bridge object exists
if (window.myBridge) {
// You can send the ArrayBuffer directly
window.myBridge.postMessage(buffer);
}
}
// Receiving binary data from the app
if (window.myBridge) {
window.myBridge.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
console.log('Received binary data from App, length:', event.data.byteLength);
// Process the binary data (for example, as a Uint8Array)
const bytes = new Uint8Array(event.data);
console.log('First byte:', bytes[0]);
}
};
}
효율적인 대규모 데이터 로드
매우 큰 파일 (>10MB)의 경우 shouldInterceptRequest 메서드를 사용하여 데이터를 스트리밍합니다.
- 웹페이지에서 맞춤 자리표시자 URL에 대한
fetch()호출을 시작합니다. 예를 들면https://app.local/large-file입니다. - Android 앱은
WebViewClient.shouldInterceptRequest에서 이 요청을 가로챕니다. - 앱은 데이터를
InputStream로 반환합니다.
이렇게 하면 전체 페이로드를 한 번에 메모리에 로드하는 대신 청크 단위로 데이터를 스트리밍할 수 있습니다.
다음 JavaScript 함수는 맞춤 자리표시자 URL에 대한 표준 fetch() 호출을 사용하여 네이티브 애플리케이션에서 대용량 바이너리 파일을 효율적으로 로드하는 클라이언트 측 코드를 보여줍니다.
async function fetchBinaryFromApp() {
try {
// This URL doesn't need to exist on the internet
const response = await fetch('https://app.local/data/large-file.bin');
if (!response.ok) throw new Error('Network response was not okay');
// For raw binary data:
const arrayBuffer = await response.arrayBuffer();
console.log('Received binary data, size:', arrayBuffer.byteLength);
// Process buffer (for example, new Uint8Array(arrayBuffer))
/*
// OR for an image:
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
document.getElementById('myImage').src = imageUrl;
*/
} catch (error) {
console.error('Fetch error:', error);
}
}
다음 코드 예에서는 Kotlin과 Java 모두에서 WebViewClient.shouldInterceptRequest 메서드를 사용하여 웹 콘텐츠에서 요청한 맞춤 자리표시자 URL을 가로채 대형 바이너리 파일을 스트리밍하는 네이티브 앱 측면을 보여줍니다.
Kotlin
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request?.url ?: return null
// Check if this is our custom placeholder URL
if (url.host == "app.local" && url.path == "/data/large-file.bin") {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
val inputStream: InputStream = context.assets.open("my_data.pb")
// 2. Define Response Headers (Crucial for CORS/Fetch)
val headers = mutableMapOf<String, String>()
headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin
// 3. Return the response
return WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
)
} catch (e: Exception) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request)
}
}
자바
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String urlPath = request.getUrl().getPath();
String host = request.getUrl().getHost();
// Check if this is our custom placeholder URL
if ("app.local".equals(host) && "/data/large-file.bin".equals(urlPath)) {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
InputStream inputStream = getContext().getAssets().open("my_data.pb");
// 2. Define Response Headers (Crucial for CORS/Fetch)
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*"); // Allow fetch from any origin
// 3. Return the response
return new WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
);
} catch (Exception e) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request);
}
});
보안 권장사항 따르기
애플리케이션과 사용자 데이터를 보호하려면 브리지를 구현할 때 다음 가이드라인을 따르세요.
HTTPS 적용: 악성 서드 파티 콘텐츠가 애플리케이션의 네이티브 로직을 호출하지 못하도록 보안 출처와의 통신만 허용합니다.
출처 규칙 사용: 신뢰를 처리하는 가장 좋은 방법은
allowedOriginRules를 엄격하게 정의하고 메시지 콜백에 제공된sourceOrigin를 확인하는 것입니다. 꼭 필요한 경우가 아니라면 모든 출처와 일치하는 전체 와일드 카드 (*)를 유일한 출처 규칙으로 사용하지 마세요. 하위 도메인에 와일드 카드 (예:*.example.com)를 사용하는 것은 여러 하위 도메인 (예:foo.example.com,bar.example.com)을 일치시키는 데 여전히 유효하고 안전합니다.참고: 출처 규칙은 악성 서드 파티 웹사이트와 숨겨진 iframe을 방지하지만, 신뢰할 수 있는 자체 도메인 내의 교차 사이트 스크립팅 (XSS) 취약점은 방지할 수 없습니다. 예를 들어 웹페이지에 사용자 제작 콘텐츠가 표시되고 저장된 XSS에 취약한 경우 공격자가 신뢰할 수 있는 출처 역할을 하는 스크립트를 실행할 수 있습니다. 민감한 네이티브 플랫폼 작업을 실행하기 전에 메시지 페이로드에 유효성 검사를 적용하는 것이 좋습니다.
표면적 최소화: 웹페이지에 필요한 특정 메서드 또는 데이터만 노출합니다.
런타임에 기능 확인:
addWebMessageListener를 비롯한 최근 브리지 API는 Jetpack Webkit 라이브러리의 일부입니다. 따라서 전화를 걸기 전에 항상WebViewFeature.isFeatureSupported()를 사용하여 지원을 확인하세요.