웹뷰 연동
매체사(WebView -> Web)
매체사(네이티브 앱) 웹뷰 호스트가 콘텐츠 웹의 자바스크립트 인터페이스를 호출하기 위한 구현 가이드입니다. AOS(Kotlin), iOS(Swift), Flutter(Dart) 샘플 코드를 제공합니다.
초기설정
- 데이터는 단일 데이터의 경우 해당 값을 String으로 전달. 다중 데이터는 JSON 형태의 String으로 전달한다.
AOS
kotlin
//init
addJavascriptInterface(CustomWebAppInterface(context, this), "addedBridge")
class CustomWebAppInterface(private val context: Context, private val webView: WebView) {
@JavascriptInterface
fun interfaceName(message : String){
//요청값 callback
webView?.evaluateJavascript(
"addedBridgeCallback.interfaceName('value')",
null
)
}
}iOS
swift
let config = WKWebViewConfiguration()
config.userContentController.add(self, name: "addedBridge")
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard message.name == "addedBridge" else { return }
// message.body는 JS에서 보낸 객체
guard let body = message.body as? [String: Any],
let method = body["method"] as? String,
let param = body["message"] as? String else { return }
switch method {
case "interfaceName":
// 요청값 callback
let value = "value"
let escaped = value.replacingOccurrences(of: "'", with: "\\'")
webView.evaluateJavaScript(
"addedBridgeCallback.interfaceName('\(escaped)')"
)
default:
break
}
}Flutter
dart
WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'addedBridge',
onMessageReceived: (JavaScriptMessage message) {
// message.message = JSON 문자열
final body = jsonDecode(message.message);
final method = body['method'] as String;
final param = body['message'] as String;
switch (method) {
case 'interfaceName':
// 요청값 callback
final value = 'value';
_controller.runJavaScript(
"addedBridgeCallback.interfaceName('$value')",
);
break;
}
},
)필수
finish()
- 현재 웹뷰를 종료합니다.
AOS
kotlin
@JavascriptInterface
fun finish(message: String) {
Log.i("addedBridge", "finish")
(context as? ComponentActivity)?.runOnUiThread {
(context as? ComponentActivity)?.finish()
}
}iOS
swift
case "finish":
DispatchQueue.main.async {
if let nav = self.navigationController {
nav.popViewController(animated: true)
} else {
self.dismiss(animated: true)
}
}Flutter
dart
case 'finish':
if (mounted) Navigator.of(context).pop();
break;registerBackKey()
- 시스템 뒤로가기(제스처/버튼)가 호출되면 네이티브에서
addedBridgeCallback.onBackKeyListener()를 호출합니다. - @param {string} true|false
AOS
kotlin
@JavascriptInterface
fun registerBackKey( message : String ) {
Log.i("addedBridge", "registerBackKey")
isBackKeyRegistered = message == "true";
}
var isBackKeyRegistered by remember { mutableStateOf(false) }
// 시스템 뒤로가기(제스처/버튼) → onBackKeyListener
BackHandler(enabled = true) {
when {
isBackKeyRegistered -> {
webView?.evaluateJavascript(
"addedBridgeCallback.onBackKeyListener()", null
)
}
canGoBack -> webView?.goBack()
else -> (webView?.context as? ComponentActivity)?.finish()
}
}iOS
swift
var isBackKeyRegistered = false
case "registerBackKey":
let flag = (body["message"] as? String) ?? "false"
isBackKeyRegistered = (flag == "true")
//시스템 뒤로가기 제스처
@objc private func onEdgeSwipe(_ gesture: UIScreenEdgePanGestureRecognizer) {
guard gesture.state == .recognized else { return }
if isBackKeyRegistered {
webView.evaluateJavaScript("addedBridgeCallback.onBackKeyListener()")
} else if webView.canGoBack {
webView.goBack()
} else {
if let nav = navigationController {
nav.popViewController(animated: true)
} else {
dismiss(animated: true)
}
}
}Flutter
dart
bool _isBackKeyRegistered = false;
case 'registerBackKey':
setState(() {
_isBackKeyRegistered = message == "true";
});
break;
// 시스템 뒤로가기 / 제스처
PopScope(
canPop: !_isBackKeyRegistered && !_canGoBack,
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
if (_isBackKeyRegistered) {
_controller.runJavaScript("addedBridgeCallback.onBackKeyListener()");
} else if (_canGoBack) {
_controller.goBack();
}
},
)registerBackButton()
- 앱바 영역에 백버튼이 존재하는 경우에만 필요합니다.
- 앱바의 백버튼이 터치되면 네이티브에서
addedBridgeCallback.onBackButtonListener()를 호출합니다. - @param {string} true|false
AOS
kotlin
var isBackButtonRegistered by remember { mutableStateOf(false) }
case 'registeBackButton':
setState(() {
isBackButtonRegistered = (param == 'true');
});
break;
IconButton(onClick =
{
if(isBackButtonRegistered){
webView?.evaluateJavascript("addedBridgeCallback.onBackButtonListener()", null)
} else if(canGoBack){
webView?.goBack()
} else {
finish()
}
}) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "뒤로가기")
}iOS
swift
var isBackButtonRegistered = false
case "registeBackButton":
let flag = (body["message"] as? String) ?? "false"
isBackButtonRegistered = (flag == "true")
//시스템 뒤로가기 제스처
@objc private func onEdgeSwipe(_ gesture: UIScreenEdgePanGestureRecognizer) {
guard gesture.state == .recognized else { return }
if isBackKeyRegistered {
webView.evaluateJavaScript("addedBridgeCallback.onBackKeyListener()")
} else if webView.canGoBack {
webView.goBack()
} else {
if let nav = navigationController {
nav.popViewController(animated: true)
} else {
dismiss(animated: true)
}
}
}
@objct private func onBackButtonTapped() {
webView.evaluateJavaScript(
"addedBridgeCallback.onBackButtonListener()"
)
}Flutter
dart
bool _isBackButtonRegistered = false;
case 'registerBackButton':
setState(() {
_isBackButtonRegistered = message == "true";
});
break;
AppBar(
title: const Text('WebView'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (_isBackKeyRegistered) {
_controller.runJavaScript("addedBridgeCallback.onBackButtonListener()");
} else if (_canGoBack) {
_controller.goBack();
} else {
Navigator.of(context).pop();
}
},
),
),선택
getAppVersion()
- 네이티브 앱의 현재 버전 정보를 조회하여 웹으로 전달합니다.
- 응답은
addedBridgeCallback.getAppVersion(version)으로 전달합니다.
AOS
kotlin
@JavascriptInterface
fun getAppVersion(message: String) {
Log.i("addedBridge", "getAppVersion")
val version = context.packageManager
.getPackageInfo(context.packageName, 0).versionName ?: ""
(context as? ComponentActivity)?.runOnUiThread {
webView?.evaluateJavascript(
"addedBridgeCallback.getAppVersion('$version')",
null
)
}
}iOS
swift
case "getAppVersion":
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
DispatchQueue.main.async {
self.webView.evaluateJavaScript(
"addedBridgeCallback.getAppVersion('\(version)')"
)
}Flutter
dart
// pubspec.yaml: package_info_plus 패키지 필요
case 'getAppVersion':
final info = await PackageInfo.fromPlatform();
final version = info.version;
_controller.runJavaScript(
"addedBridgeCallback.getAppVersion('$version')",
);
break;getADID()
- 디바이스 광고 식별값(ADID)을 조회하여 웹으로 전달합니다.
- 응답은
addedBridgeCallback.getADID(adId)으로 전달합니다.
AOS
kotlin
// Google Play Services Ads Identifier 의존성 필요
// implementation("com.google.android.gms:play-services-ads-identifier:18.x.x")
@JavascriptInterface
fun getADID(message: String) {
Log.i("addedBridge", "getADID")
Thread {
try {
val info = AdvertisingIdClient.getAdvertisingIdInfo(context)
val adId = info.id ?: ""
(context as? ComponentActivity)?.runOnUiThread {
webView?.evaluateJavascript(
"addedBridgeCallback.getADID('$adId')", null
)
}
} catch (e: Exception) { e.printStackTrace() }
}.start()
}iOS
swift
// import AdSupport
// iOS 14+ 의 경우 ATTrackingManager 권한 요청 필요
case "getADID":
let adId = ASIdentifierManager.shared().advertisingIdentifier.uuidString
DispatchQueue.main.async {
self.webView.evaluateJavaScript(
"addedBridgeCallback.getADID('\(adId)')"
)
}Flutter
dart
// pubspec.yaml: advertising_id 패키지 필요
case 'getADID':
final adId = await AdvertisingId.id(true) ?? '';
_controller.runJavaScript(
"addedBridgeCallback.getADID('$adId')",
);
break;getIsAgeUnder14()
- 유저의 나이가 14세 미만인지 여부를 조회하여 웹으로 전달합니다. ("true" | "false")
- 응답은
addedBridgeCallback.getIsAgeUnder14(isUnder14)으로 전달합니다.
AOS
kotlin
@JavascriptInterface
fun getIsAgeUnder14(message: String) {
Log.i("addedBridge", "getIsAgeUnder14")
// 앱에서 관리하는 유저 나이 정책에 맞게 구현
val isUnder14 = userRepository.isAgeUnder14() // true | false
(context as? ComponentActivity)?.runOnUiThread {
webView?.evaluateJavascript(
"addedBridgeCallback.getIsAgeUnder14('$isUnder14')", null
)
}
}iOS
swift
case "getIsAgeUnder14":
// 앱에서 관리하는 유저 나이 정책에 맞게 구현
let isUnder14 = UserRepository.shared.isAgeUnder14 ? "true" : "false"
DispatchQueue.main.async {
self.webView.evaluateJavaScript(
"addedBridgeCallback.getIsAgeUnder14('\(isUnder14)')"
)
}Flutter
dart
case 'getIsAgeUnder14':
// 앱에서 관리하는 유저 나이 정책에 맞게 구현
final isUnder14 = userRepository.isAgeUnder14 ? 'true' : 'false';
_controller.runJavaScript(
"addedBridgeCallback.getIsAgeUnder14('$isUnder14')",
);
break;getDeviceInfo()
- 기기 정보(network / model / manufacturer / osVersion)를 조회하여 웹으로 전달합니다.
- 응답은 JSON 문자열로
addedBridgeCallback.getDeviceInfo(deviceInfoJson)으로 전달합니다. - ex)
{"network":"wifi","model":"SM-S911N","manufacturer":"Samsung","osVersion":"14"}
AOS
kotlin
@JavascriptInterface
fun getDeviceInfo(message: String) {
Log.i("addedBridge", "getDeviceInfo")
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val caps = cm.getNetworkCapabilities(cm.activeNetwork)
val network = when {
caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "wifi"
caps?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "mobile"
else -> ""
}
val json = JSONObject().apply {
put("network", network)
put("model", Build.MODEL)
put("manufacturer", Build.MANUFACTURER)
put("osVersion", Build.VERSION.RELEASE)
}.toString().replace("'", "\\'")
(context as? ComponentActivity)?.runOnUiThread {
webView?.evaluateJavascript(
"addedBridgeCallback.getDeviceInfo('$json')", null
)
}
}iOS
swift
// import Network
case "getDeviceInfo":
let network = NetworkMonitor.shared.currentType() // "wifi" | "mobile"
let info: [String: String] = [
"network": network,
"model": UIDevice.current.model,
"manufacturer": "Apple",
"osVersion": UIDevice.current.systemVersion
]
if let data = try? JSONSerialization.data(withJSONObject: info),
let json = String(data: data, encoding: .utf8) {
let escaped = json.replacingOccurrences(of: "'", with: "\\'")
DispatchQueue.main.async {
self.webView.evaluateJavaScript(
"addedBridgeCallback.getDeviceInfo('\(escaped)')"
)
}
}Flutter
dart
// pubspec.yaml: device_info_plus, connectivity_plus 패키지 필요
case 'getDeviceInfo':
final deviceInfo = DeviceInfoPlugin();
final connectivity = await Connectivity().checkConnectivity();
final network = connectivity == ConnectivityResult.wifi ? 'wifi' : 'mobile';
String model = '', manufacturer = '', osVersion = '';
if (Platform.isAndroid) {
final android = await deviceInfo.androidInfo;
model = android.model;
manufacturer = android.manufacturer;
osVersion = android.version.release;
} else if (Platform.isIOS) {
final ios = await deviceInfo.iosInfo;
model = ios.model;
manufacturer = 'Apple';
osVersion = ios.systemVersion;
}
final json = jsonEncode({
'network': network,
'model': model,
'manufacturer': manufacturer,
'osVersion': osVersion,
}).replaceAll("'", "\\'");
_controller.runJavaScript(
"addedBridgeCallback.getDeviceInfo('$json')",
);
break;pushUrl()
- 현재 웹뷰를 재사용하여 전달받은 URL로 이동합니다.
- @param {string} JSON string { url, replace? } — replace=true 면 history 스택 교체, false면 push
AOS
kotlin
@JavascriptInterface
fun pushUrl(message: String) {
Log.i("addedBridge", "pushUrl")
val obj = JSONObject(message)
val url = obj.optString("url")
val replace = obj.optBoolean("replace", false)
(context as? ComponentActivity)?.runOnUiThread {
if (replace) {
webView?.loadUrl(url)
// replace=true: 이동 완료 후 history 정리 필요시 clearHistory()
} else {
webView?.loadUrl(url)
}
}
}iOS
swift
case "pushUrl":
guard let data = param.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let urlStr = json["url"] as? String,
let url = URL(string: urlStr) else { return }
let replace = json["replace"] as? Bool ?? false
DispatchQueue.main.async {
self.webView.load(URLRequest(url: url))
// replace=true: WKWebView 제약으로 별도 history 관리 정책 필요
}Flutter
dart
case 'pushUrl':
final data = jsonDecode(message);
final url = data['url'] as String;
final replace = (data['replace'] as bool?) ?? false;
_controller.loadRequest(Uri.parse(url));
// replace=true 의 경우 history 관리 정책 필요
break;openInternalBrowser()
- 새로운 앱 화면(Activity / UIViewController)에 웹뷰를 띄웁니다.
- @param {string} url
AOS
kotlin
@JavascriptInterface
fun openInternalBrowser(message: String) {
Log.i("addedBridge", "openInternalBrowser")
val url = message
(context as? ComponentActivity)?.runOnUiThread {
val intent = Intent(context, WebViewActivity::class.java).apply {
putExtra("url", url)
}
context.startActivity(intent)
}
}iOS
swift
case "openInternalBrowser":
guard let url = URL(string: param) else { return }
DispatchQueue.main.async {
let vc = WebViewController(url: url)
if let nav = self.navigationController {
nav.pushViewController(vc, animated: true)
} else {
self.present(vc, animated: true)
}
}Flutter
dart
case 'openInternalBrowser':
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => WebViewPage(url: message),
));
break;setTitle()
- 앱바 영역의 타이틀을 수정합니다. 앱바를 앱에서 관리하고 타이틀이 존재할 때만 사용합니다.
- @param {string} title
AOS
kotlin
@JavascriptInterface
fun setTitle(message: String) {
Log.i("addedBridge", "setTitle")
(context as? ComponentActivity)?.runOnUiThread {
// Compose 상태값 업데이트 등 앱바 구현에 맞게 반영
appBarTitle.value = message
}
}iOS
swift
case "setTitle":
DispatchQueue.main.async {
self.title = param
self.navigationItem.title = param
}Flutter
dart
case 'setTitle':
setState(() {
_title = message;
});
break;관련 문서
- 웹뷰 연동 — 기본 설정, 샘플 코드
- 콘텐츠사(Web → WebView) — 콘텐츠 웹에서 호출하는 자바스크립트 인터페이스 명세