웹뷰 연동
매체사(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;보상형 광고
콘텐츠 웹이 호출한 AdMob 보상형(rewarded) 광고를 전체 화면으로 표시합니다. 전체 화면과 닫기 UI 를 AdMob SDK 가 소유하므로, 네이티브 오버레이와 달리 웹에서 rect 를 받지 않습니다. 결과는 requestId 기준으로 콜백합니다.
수명주기: adInit(1회) → adLoad(format: rewarded) → adShowRewarded → 닫힘 시 onAdClosed, 보상 조건 충족 시 onAdRewarded.
AdMob 광고 형식 구현 (필수)
- 보상형(rewarded)은
RewardedAd/ iOSGADRewardedAd를 사용하며, 전체화면·닫기 UI 는 SDK 가 소유합니다. 노출/클릭/닫힘/실패는FullScreenContentCallback로 통지합니다. onUserEarnedReward시점에onAdRewarded를 보냅니다. 이는 클라이언트 신호이므로, 실제 보상 지급은 서버(S2S) 검증 후 처리하도록 안내합니다.
구현 노트
- 로드한 광고는
requestId로 캐시하고, 표시 메서드에서 같은requestId로 찾아 씁니다. 준비되지 않았으면onAdFailed(stage:'show',code:'NOT_READY')로 통지합니다. rewarded는 1회성이라 표시할 때 캐시에서 제거하고, 닫힘/보상은 SDK 콜백으로 받습니다(전체화면·닫기 UI 와 리소스는 SDK 가 자동 소유·해제).
adInit()
- 부팅 시 1회. AdMob SDK 를 초기화합니다(동의/ATT 는 호스트 책임).
- test=true 면 테스트 디바이스/광고 단위를 사용합니다.
AOS
kotlin
// build.gradle: implementation("com.google.android.gms:play-services-ads:23.x.x")
// AndroidManifest: <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" .../>
// 앱 시작 시 1회
MobileAds.initialize(context) {}
@JavascriptInterface
fun adInit(message: String) {
Log.i("addedBridge", "adInit")
// message = { test, networks:[{ key:"admob", app:{ appId } }] }
val obj = JSONObject(message)
if (obj.optBoolean("test")) {
MobileAds.setRequestConfiguration(
RequestConfiguration.Builder()
.setTestDeviceIds(listOf(AdRequest.DEVICE_ID_EMULATOR))
.build()
)
}
}iOS
swift
// Podfile: pod 'Google-Mobile-Ads-SDK'
// Info.plist: GADApplicationIdentifier = ca-app-pub-XXXX~YYYY
// 앱 시작 시 1회
GADMobileAds.sharedInstance().start(completionHandler: nil)
case "adInit":
guard let data = param.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return }
if json["test"] as? Bool == true {
GADMobileAds.sharedInstance()
.requestConfiguration.testDeviceIdentifiers = ["SIMULATOR"]
}Flutter
dart
// pubspec.yaml: google_mobile_ads
// 앱 시작 시 1회
await MobileAds.instance.initialize();
case 'adInit':
final obj = jsonDecode(message);
if (obj['test'] == true) {
MobileAds.instance.updateRequestConfiguration(
RequestConfiguration(testDeviceIds: ['SIMULATOR']),
);
}
break;adLoad() — format: rewarded
format: 'rewarded'이면RewardedAd를 로드해requestId로 캐시합니다(화면엔 표시하지 않음).- 결과는
onAdLoaded/onAdFailed콜백으로 통지합니다.
AOS
kotlin
private val rewardedAds = HashMap<String, RewardedAd>() // format: 'rewarded'
// 콜백 헬퍼 — payload 를 객체로 전달(웹은 객체/JSON 문자열 모두 허용)
private fun emit(name: String, requestId: String, vararg extra: Pair<String, Any>) {
val payload = JSONObject().put("requestId", requestId)
extra.forEach { payload.put(it.first, it.second) }
(context as? ComponentActivity)?.runOnUiThread {
webView?.evaluateJavascript("addedBridgeCallback.$name($payload)", null)
}
}
@JavascriptInterface
fun adLoad(message: String) {
val obj = JSONObject(message)
val requestId = obj.getString("requestId")
val adUnitId = obj.getJSONObject("ids").getString("adUnitId")
val req = AdRequest.Builder().build()
(context as? ComponentActivity)?.runOnUiThread {
when (obj.optString("format")) {
"rewarded" -> RewardedAd.load(context, adUnitId, req,
object : RewardedAdLoadCallback() {
override fun onAdLoaded(ad: RewardedAd) {
rewardedAds[requestId] = ad; emit("onAdLoaded", requestId)
}
override fun onAdFailedToLoad(e: LoadAdError) =
emit("onAdFailed", requestId, "stage" to "load", "code" to e.code.toString())
})
}
}
}iOS
swift
var rewardeds: [String: GADRewardedAd] = [:] // 'rewarded'
// 콜백 헬퍼 — payload 객체 전달
func emit(_ name: String, _ requestId: String, _ extra: [String: Any] = [:]) {
var payload = extra; payload["requestId"] = requestId
guard let data = try? JSONSerialization.data(withJSONObject: payload),
let json = String(data: data, encoding: .utf8) else { return }
DispatchQueue.main.async {
self.webView.evaluateJavaScript("addedBridgeCallback.\(name)(\(json))")
}
}
case "adLoad":
guard let data = param.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let requestId = json["requestId"] as? String,
let format = json["format"] as? String,
let ids = json["ids"] as? [String: Any],
let adUnitId = ids["adUnitId"] as? String else { return }
let req = GADRequest()
switch format {
case "rewarded":
GADRewardedAd.load(withAdUnitID: adUnitId, request: req) { [weak self] ad, err in
guard let self = self else { return }
if let ad = ad { self.rewardeds[requestId] = ad; self.emit("onAdLoaded", requestId) }
else { self.emit("onAdFailed", requestId, ["stage": "load", "code": "\(err!)"]) }
}
default: break
}Flutter
dart
final Map<String, RewardedAd> _rewardeds = {}; // 'rewarded'
// 콜백 헬퍼 — payload 객체 전달
void _emit(String name, String requestId, [Map<String, dynamic> extra = const {}]) {
final payload = jsonEncode({'requestId': requestId, ...extra});
_controller.runJavaScript('addedBridgeCallback.$name($payload)');
}
case 'adLoad':
final obj = jsonDecode(message);
final requestId = obj['requestId'] as String;
final adUnitId = obj['ids']['adUnitId'] as String;
switch (obj['format']) {
case 'rewarded':
RewardedAd.load(
adUnitId: adUnitId,
request: const AdRequest(),
rewardedAdLoadCallback: RewardedAdLoadCallback(
onAdLoaded: (ad) { _rewardeds[requestId] = ad; _emit('onAdLoaded', requestId); },
onAdFailedToLoad: (err) => _emit('onAdFailed', requestId, {'stage': 'load', 'code': '${err.code}'}),
),
);
break;
}
break;adShowRewarded()
- 로드된 보상형 광고를 SDK 로 전체화면 표시합니다. 전체화면·닫기 UI 는 SDK 가 소유하며, 결과는
FullScreenContentCallback(노출/클릭/닫힘/실패)로 통지합니다. onUserEarnedReward시점에onAdRewarded({ requestId, type, amount }) 를 함께 보냅니다.- 1회성이므로 표시 시 캐시에서 제거하고 SDK 가 리소스를 해제합니다.
AOS
kotlin
// 보상형 공통: 전체화면 표시 결과 콜백
private fun fullScreenCallback(requestId: String) = object : FullScreenContentCallback() {
override fun onAdShowedFullScreenContent() = emit("onAdShown", requestId)
override fun onAdClicked() = emit("onAdClicked", requestId)
override fun onAdDismissedFullScreenContent() = emit("onAdClosed", requestId)
override fun onAdFailedToShowFullScreenContent(e: AdError) =
emit("onAdFailed", requestId, "stage" to "show", "code" to e.code.toString())
}
@JavascriptInterface
fun adShowRewarded(message: String) {
val requestId = JSONObject(message).getString("requestId")
val ad = rewardedAds.remove(requestId) // 1회성 — 표시하며 캐시에서 제거
?: return emit("onAdFailed", requestId, "stage" to "show", "code" to "NOT_READY")
(context as? ComponentActivity)?.runOnUiThread {
ad.fullScreenContentCallback = fullScreenCallback(requestId)
ad.show(context as Activity) { reward -> // 보상 조건 충족
emit("onAdRewarded", requestId, "type" to reward.type, "amount" to reward.amount)
}
}
}iOS
swift
// GADFullScreenContentDelegate — ad → requestId 는 ridOf 로 매핑
// ridParam(_:) 은 param(JSON)에서 requestId 를 파싱하는 헬퍼
func adDidRecordImpression(_ ad: GADFullScreenPresentingAd) { emit("onAdShown", ridOf(ad)) }
func adDidRecordClick(_ ad: GADFullScreenPresentingAd) { emit("onAdClicked", ridOf(ad)) }
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) { emit("onAdClosed", ridOf(ad)) }
func ad(_ ad: GADFullScreenPresentingAd,
didFailToPresentFullScreenContentWithError error: Error) {
emit("onAdFailed", ridOf(ad), ["stage": "show", "code": "\(error)"])
}
case "adShowRewarded":
guard let requestId = ridParam(param),
let ad = rewardeds.removeValue(forKey: requestId) else { return }
ad.fullScreenContentDelegate = self // SDK 가 전체화면/닫기 UI 를 소유
DispatchQueue.main.async {
ad.present(fromRootViewController: self) {
let r = ad.adReward // 보상 조건 충족
self.emit("onAdRewarded", requestId, ["type": r.type, "amount": r.amount])
}
}Flutter
dart
// 보상형 공통: 전체화면 표시 결과 콜백
FullScreenContentCallback<T> _fsCallback<T extends AdWithoutView>(String requestId) =>
FullScreenContentCallback<T>(
onAdShowedFullScreenContent: (ad) => _emit('onAdShown', requestId),
onAdClicked: (ad) => _emit('onAdClicked', requestId),
onAdDismissedFullScreenContent: (ad) { ad.dispose(); _emit('onAdClosed', requestId); },
onAdFailedToShowFullScreenContent: (ad, err) {
ad.dispose();
_emit('onAdFailed', requestId, {'stage': 'show', 'code': '${err.code}'});
},
);
case 'adShowRewarded':
final requestId = jsonDecode(message)['requestId'] as String;
final ad = _rewardeds.remove(requestId); // 1회성 — 표시하며 캐시에서 제거
if (ad == null) { _emit('onAdFailed', requestId, {'stage': 'show', 'code': 'NOT_READY'}); break; }
ad.fullScreenContentCallback = _fsCallback<RewardedAd>(requestId);
ad.show(onUserEarnedReward: (ad, reward) { // 보상 조건 충족
_emit('onAdRewarded', requestId, {'type': reward.type, 'amount': reward.amount});
});
break;콜백 (App → Web)
호스트는 각 광고의 후속 이벤트를 동일 requestId 를 실어 통지합니다.
onAdLoaded—{ requestId }로드 성공(fill).onAdFailed—{ requestId, stage, code }로드/부착 실패.stage='load'|'show'.onAdShown—{ requestId }노출(AdMob onAdShowedFullScreenContent).onAdClicked—{ requestId }클릭.onAdClosed—{ requestId }광고가 닫힘. 콘텐츠 흐름 재개 신호로 사용합니다.onAdRewarded—{ requestId, type, amount }보상형 광고에서 보상 조건 충족(클라이언트 신호).
닫힘 · 보상 콜백 요약
보상형 광고는 전체 화면과 닫기 UI 를 AdMob SDK 가 소유합니다. 광고가 닫히면
onAdClosed, 보상 조건을 충족하면 onAdRewarded 를 보냅니다. 표시 시 1회성으로 소비되며 SDK 가 리소스를 자동 해제합니다.관련 문서
- 웹뷰 연동 — 기본 설정, 샘플 코드
- 콘텐츠사(Web → WebView) — 콘텐츠 웹에서 호출하는 자바스크립트 인터페이스 명세