웹뷰 연동

매체사(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 / iOS GADRewardedAd 를 사용하며, 전체화면·닫기 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 가 리소스를 자동 해제합니다.

관련 문서