이 페이지에서는 WebView의 웹 콘텐츠와 호스트 Android 애플리케이션 간의 통신을 용이하게 하기 위해 JavaScript 브리지라고도 하는 네이티브 브리지를 설정하는 다양한 방법과 권장사항을 설명합니다.
이를 통해 웹 개발자는 JavaScript를 사용하여 표준 웹 API가 일반적으로 제공하지 않는 카메라, 파일 시스템, 고급 하드웨어 센서와 같은 네이티브 플랫폼 기능에 액세스할 수 있습니다.
사용 사례
JavaScript 브리지 구현을 사용하면 웹 콘텐츠가 Android 운영체제에 더 깊이 액세스해야 하는 다양한 통합 시나리오가 가능합니다. 다음은 몇 가지 예입니다.
- 플랫폼 통합: 웹페이지에서 네이티브 Android UI 구성요소 (예: 생체 인식 프롬프트,
BottomSheetDialog)를 트리거합니다. - 성능: 무거운 계산 작업을 네이티브 Java 또는 Kotlin 코드로 오프로드합니다.
- 데이터 지속성: 로컬 암호화 데이터베이스 또는 공유 환경설정에 액세스합니다.
- 대량 데이터 전송: 앱과 웹 렌더러 간에 미디어 파일 또는 복잡한 데이터 구조를 전달합니다.
커뮤니케이션 메커니즘
Android는 네이티브 브리지를 설정하기 위해 세 가지 기본 세대의 API를 제공합니다. 이러한 방법은 모두 사용할 수 있지만 보안, 사용성, 성능이 크게 다릅니다.
addWebMessageListener 사용 (권장)
addWebMessageListener는 웹 콘텐츠와 네이티브 앱 코드 간 통신에 가장 최신이며 권장되는 접근 방식입니다. JavaScript 인터페이스의 사용 편의성과 메시지 시스템의 보안을 결합합니다.
작동 방식: 앱은 특정 이름과 허용된 출처 규칙 집합을 사용하여 리스너를 추가합니다. 그러면 WebView는 페이지가 로드되기 시작하는 순간부터 JavaScript 객체가 전역 범위 (window.objectName)에 있는지 확인합니다.
초기화: WebView가 스크립트가 실행되기 전에 JavaScript 객체를 삽입하도록 하려면 페이지로 이동하기 전에 addWebMessageListener를 호출해야 합니다 (예: WebViewCompat.navigate 또는 loadUrl 호출).
주요 기능
보안 및 신뢰: 기존 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로 리스너를 등록하고 페이지 탐색을 시작합니다 (예:WebViewCompat.navigate또는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()과 같은 메서드는 정확성이 보장되지 않고 요청을 한 특정 프레임을 나타내지 않으므로 보안 확인에 사용해서는 안 됩니다.
데이터 유형 변환 및 강제 변환
addJavascriptInterface를 사용하면 Chromium 기반 Java 브리지가 JavaScript 런타임과 Android 앱 코드 간에 데이터 유형을 변환합니다.
다음 강제 변환 규칙은 메서드 매개변수와 반환 값에 적용됩니다.
매개변수 유형 매핑 (JavaScript에서 Java로)
JavaScript가 주석이 달린 Java 또는 Kotlin 메서드에 인수를 전달하면 브리지는 JavaScript 값을 해당 Java 매개변수 유형으로 강제 변환합니다.
| Java 매개변수 유형 | JavaScript 인수 값 | 강제 변환 동작 |
|---|---|---|
byte, short, int, long |
숫자 (정수) | 값은 타겟 정수 유형으로 변환됩니다. 범위를 벗어난 값은 표준 숫자 변환 규칙에 따라 래핑됩니다. |
byte, short, int, long |
NaN |
0로 강제 변환됩니다. |
byte, short, int, long |
Infinity |
byte 및 short의 경우 -1로 강제 변환하거나 int 및 long의 경우 Integer.MAX_VALUE 및 Long.MAX_VALUE로 강제 변환합니다. |
double float |
숫자 | 해당하는 Java 부동 소수점 값으로 강제 변환합니다. |
double float |
NaN/Infinity |
Float.NaN, Double.NaN, Float.POSITIVE_INFINITY 또는 Double.POSITIVE_INFINITY로 강제 변환됩니다. |
char |
숫자 (정수) | 해당 유니코드 코드 포인트로 변환됩니다. |
char |
정수가 아님, NaN, Infinity |
\u0000로 강제 변환됩니다. |
boolean |
true/false |
Java true 또는 false로 강제 변환됩니다. |
boolean |
숫자, 문자열, 객체 | false (비어 있지 않은 문자열과 0이 아닌 숫자 포함)로 강제 변환됩니다. |
String |
문자열 | 문자열 값이 보존됩니다. |
String |
숫자, 불리언 | 문자열 표현 (예: "42", "true", "false")으로 형식이 지정됩니다. |
String |
null/undefined |
null은 Java null로 강제 변환되고 undefined은 리터럴 문자열 "undefined"로 강제 변환됩니다. |
String |
객체, ArrayBuffer, TypedArray | 리터럴 문자열 "undefined"로 강제 변환됩니다. |
기본 배열 (예: int[], byte[], boolean[]) 또는 String[] |
배열 ([...]) |
타겟 요소 유형의 1D Java 배열로 변환합니다. 희소 배열은 할당되지 않은 색인을 기본값 (0, false, null)으로 채웁니다. |
기본 배열 (예: int[], byte[]) |
TypedArray (Int8Array, Uint8Array, Int32Array, Float64Array) |
요소는 해당 Java 기본 배열로 강제 변환됩니다. |
다차원 배열 (예: int[][]) |
중첩 배열 ([[...]]) |
지원되지 않음 다차원 배열 매개변수는 null로 평가됩니다. |
DataView ArrayBuffer |
DataView ArrayBuffer |
배열로 지원되지 않음 ArrayBuffer 및 DataView 인스턴스는 null로 평가됩니다. |
Object 또는 맞춤 클래스 |
JavaScript 객체 ({...}) |
지원되지 않음 임의의 JavaScript 객체 리터럴은 Java에서 null로 평가됩니다. |
Object 또는 맞춤 클래스 |
삽입된 Java 객체 래퍼 | 지원됨 (라운드트리핑) 기본 Java 인스턴스를 Java 메서드에 전달합니다. Java 유형이 매개변수 서명과 일치하지 않으면 JavaScript 예외를 발생시킵니다. |
박스형 유형 (예: Integer, Double, Boolean) |
숫자, 불리언 | 지원되지 않음 박싱된 원시 유형은 불투명 객체로 처리되고 null로 평가됩니다. |
| 모든 기본 유형 | null/undefined |
기본값 (0, 0.0, \u0000, false)으로 강제 변환됩니다. |
Object, String, 배열 |
null |
Java null로 강제 변환됩니다. |
반환 유형 매핑 (Java에서 JavaScript로)
주석이 추가된 Java 또는 Kotlin 메서드가 값을 반환하면 브리지가 이를 JavaScript 유형으로 변환합니다.
| Java 반환 유형 | JavaScript 값 | JavaScript typeof |
|---|---|---|
boolean |
true/false |
"boolean" |
byte, short, int, long, float, double |
숫자 | "number" |
char |
숫자 (유니코드 코드 포인트) | "number" |
String (null 아님) |
문자열 값 | "string" |
String(null) |
undefined |
"undefined" |
void |
undefined |
"undefined" |
Java 배열 (예: int[], String[]) |
undefined |
"undefined". 배열 반환 값은 지원되지 않습니다. Java 메서드가 실행되지 않고 예외를 발생시키지 않고 undefined가 반환됩니다. |
| Java 객체 / 맞춤 유형 (null 아님) | 객체 래퍼 | "object". Java 인스턴스 주위에 JavaScript 래퍼를 만듭니다. JavaScript 코드는 @JavascriptInterface로 주석이 추가된 이 객체의 공개 메서드를 호출할 수 있습니다. |
Java 객체 / 맞춤 유형 (null) |
null |
"object" |
박스형 기본 (예: Integer, Double) |
객체 래퍼 | "object". 액세스 가능한 @JavascriptInterface 메서드가 없는 불투명한 Java 객체 래퍼로 반환되어 JavaScript에서 값을 사용할 수 없습니다. |
메서드 및 멤버 접근성
JavaScript 브리지는 의도치 않은 코드 실행을 방지하기 위해 엄격한 멤버 액세스 및 공개 상태 규칙을 적용합니다.
- 필드가 노출되지 않음: Java 필드 (
public및public final필드 포함)는 JavaScript에서 액세스할 수 없으며undefined로 평가됩니다. - 주석 요구사항:
@JavascriptInterface로 명시적으로 주석이 지정된 메서드만 JavaScript에 노출됩니다. - 공개 상태 제한: 메서드는
public이어야 합니다.private및protected메서드는@JavascriptInterface주석을 포함하더라도 JavaScript에 노출되지 않습니다. - 정적 메서드:
@JavascriptInterface로 주석이 달린 정적 메서드는 JavaScript에서 호출할 수 있습니다. - 상속 및 재정의: 서브클래스가 메서드를 재정의할 때
@JavascriptInterface주석이 상속되지 않습니다. 서브클래스가 슈퍼클래스의 주석이 달린 메서드를 재정의하는 경우 서브클래스는 재정의된 메서드에@JavascriptInterface주석을 명시적으로 포함하여 JavaScript에 노출해야 합니다. 슈퍼클래스에서 주석이 지정된 경우 재정의되지 않은 슈퍼클래스에서 상속된 공개 메서드는 계속 액세스할 수 있습니다. - 리플렉션 보호: 표준 Java 리플렉션 메서드 (예:
getClass())가 차단되고 JavaScript 예외가 발생하여 원격 코드 실행 취약점을 방지합니다. - 메서드 오버로드: 오버로드된 Java 메서드가 지원됩니다. 브리지는 전달된 인수 수에만 기반하여 메서드 호출을 해결하고 인수 유형은 고려하지 않습니다. 잘못된 인수 개수로 오버로드된 메서드를 호출하면 JavaScript 예외가 발생합니다. 오버로드 두 개의 인수 개수가 동일한 경우 하나가 임의로 선택됩니다.
메커니즘 요약
다음 표에서는 세 가지 기본 네이티브 브리지 구현 메커니즘을 간단히 비교합니다.
| 메서드 | 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) 경계에서 데이터를 복사합니다. 이 경우에도 매우 큰 파일의 경우 상당한 메모리를 소비합니다.
다음 코드 예에서는 WebMessageCompat.TYPE_ARRAY_BUFFER로 표시된 메시지를 수신하고 WebViewFeature.MESSAGE_ARRAY_BUFFER를 확인하여 선택적으로 바이너리 데이터로 응답하도록 네이티브 앱 측에서 addWebMessageListener를 설정하는 방법을 보여줍니다.
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()를 사용하여 지원을 확인하세요.