웹뷰 연동

콘텐츠사(Web -> WebView)

콘텐츠 웹에서 매체사 네이티브 앱(웹뷰 호스트)을 호출하기 위한 자바스크립트 인터페이스 명세입니다.

권장 연동 — @blomics/added-bridge

매체마다 웹뷰 브릿지가 다릅니다. ad.ded 표준(window.addedBridge)을 구현한 매체도 있지만, 케이뱅크(window.kbw)·모니모(window.MonimoAppInterface)처럼 자체 규격을 가진 매체도 있습니다. @blomics/added-bridge 는 이 차이를 하나의 인터페이스로 감싸므로, 콘텐츠는 매체를 몰라도 같은 코드로 연동할 수 있습니다. 아래 원시 인터페이스를 직접 호출하기보다 이 라이브러리를 쓰는 것을 권장합니다.

초기화

  • ESM: dist/added-bridge.es.js (타입 dist/index.d.ts). 게임 레포에는 git 서브모듈로 넣고 빌드 산출물(dist/)을 그대로 씁니다.
  • initBridge() 는 부팅 시 1회만 호출합니다(두 번 호출하면 매체 리스너가 중복 등록됩니다). 이후에는 getBridge() 로 같은 인스턴스를 씁니다.
  • 매체 판별 순서: publisherSlug(가장 신뢰도 높음) → 주입된 브릿지 객체 탐지(매체 전용 규격 우선) → ad.ded 표준. 일반 브라우저도 표준으로 판별되며 bridge.available 가 false 입니다.
typescript
import { initBridge, getBridge } from '@blomics/added-bridge';

// 부팅 시 1회. 매체 slug 를 알면 넘기고(KBANK · MONIMO …), 모르면 생략한다 — 주입된 브릿지 형태로 판별한다.
const bridge = initBridge({ publisherSlug: 'KBANK' });

bridge.onBackKey(() => confirmExit());          // 시스템 백 · 제스처 · 앱바 백 단일 진입 (해제 함수 반환)
bridge.openExternal('https://added.blomics.net/cs/inquiry');
bridge.exit();                                   // 웹뷰 종료 — 매체 앱의 이전 화면으로 복귀

const version = await bridge.getAppVersion();   // 미지원 · 무응답이면 null
if (bridge.supports('deviceInfo')) {
  const info = await bridge.getDeviceInfo();     // { network, model, manufacturer, osVersion, raw }
}

// 이후에는 어디서든 같은 인스턴스를 얻는다
getBridge().setTitle('게임');
html
<!-- 번들러가 없는 엔진(Unity WebGL, Godot …): 게임 로더보다 먼저 읽는다 -->
<script src="added-bridge.iife.js"></script>
<script>
  var bridge = window.AddedBridge.initBridge({ publisherSlug: null });
</script>

통합 API ↔ 매체 메서드

ad.ded 표준 열은 이 페이지 아래에 정리한 원시 메서드입니다. 매체가 표준을 그대로 구현했다면 전용 처리 없이 동작합니다.

통합 APIad.ded 표준케이뱅크모니모
exit()finishkbankclosewebview1404
openExternal(url)openExternalBrowserkbankopenbrowser1405
openInternal(url)openInternalBrowser→ 외부 브라우저→ 외부 브라우저
pushUrl(url, { replace })pushUrl미지원미지원
setTitle(title)setTitle미지원미지원
hide()미지원kbankhidewebview미지원
onBackKey(handler)registerBackKey · registerBackButtonf_onBackPressed + 스와이프백 가드1910 + monimo.bridge.ptn.back
getAppVersion()getAppVersion→ null→ null
getAdId()getADID→ null→ null
isAgeUnder14()getIsAgeUnder14→ null→ null
getDeviceInfo()getDeviceInfo→ null1911 + 1912
getAuthToken()→ null→ null1906 OTT (30초 · 1회용)
ads.init / load / showadInit · adLoad · adShowRewarded(보상형) · adShow(전면형)kbankrewardad (reward · basic)1916 → 1917 (rewarded · interstitial)

미지원 기능은 던지지 않습니다

  • 매체가 지원하지 않는 호출은 예외 없이 흡수됩니다. 변경 계열(setTitle 등)은 아무 동작도 하지 않고 경고를 1회 남기며, 조회 계열은 null 을 돌려줍니다. 매체별 try/catch 가 필요 없습니다.
  • 분기가 필요하면 bridge.supports('pushUrl') 처럼 미리 확인합니다.
  • 조회·광고 로드·광고 노출에는 모두 상한이 있어(기본 3초 · 5초 · 10초) 호스트가 응답하지 않아도 반드시 종결됩니다.
  • 통합 API 에 아직 없는 매체 고유 기능은 bridge.raw.call(method, message) / bridge.raw.on(event, handler) 로 직접 부릅니다.

광고 — bridge.ads

매체마다 광고 프로토콜이 전혀 다르지만 계약은 하나입니다. load() 는 no-fill 이면 null, show() 는 항상 결과 객체로 끝납니다. 서버에 보상을 요청하는 분기는 반드시 rewarded 로 가릅니다 — 스킵·중도 이탈도 shown: true 로 돌아옵니다.

  • format: 'rewarded'(보상형) — 끝까지 시청하면 rewarded: true. 보상 지면에 씁니다.
  • format: 'interstitial'(전면형) — 보상이 없는 전체 화면 광고입니다. 게임 오버 · 스테이지 전환처럼 흐름 사이에 끼우고, show() 가 끝나면(광고 닫힘) 흐름을 재개합니다. rewarded 는 항상 false 이고 completed 는 없습니다.
typescript
bridge.ads.init({
  test: false,
  networks: [{ key: 'admob', surface: 'bridge', app: { appId: 'ca-app-pub-XXXX~YYYY' } }],
});

// 보상형
const ad = await bridge.ads.load({ format: 'rewarded', ids: { adUnitId: 'ca-app-pub-XXXX/ZZZZ' } });
if (!ad) return;                       // no-fill · 로드 상한(5초) 초과 → 다음 후보로

const result = await ad.show();        // { shown, rewarded, clicked, error? } — 노출 상한(10초)으로 항상 종결
if (result.rewarded) {
  requestServerReward();               // ⚠️ 클라이언트 신호일 뿐 — 실제 지급은 서버 검증 후
}

// 전면형 — 보상 없이 노출만. show() 가 끝나면(광고 닫힘) 콘텐츠 흐름을 재개한다
const inter = await bridge.ads.load({ format: 'interstitial', ids: { adUnitId: 'ca-app-pub-XXXX/WWWW' } });
if (inter) await inter.show();         // rewarded 는 항상 false
resumeContent();
광고 워터폴은 added-mediation
여러 광고 네트워크의 워터폴·빈도 제한·백필은 @blomics/added-mediation 이 담당합니다. @blomics/added-bridge는 그 아래에서 "매체 브릿지 1개를 매체와 무관하게 부르는 층"이며, 포인트를 계산하거나 이벤트를 보내지 않습니다.

원시 인터페이스 (ad.ded 표준)

라이브러리를 쓰지 않거나 호출을 직접 확인할 때의 규격입니다. 라이브러리도 내부에서 이 규격 그대로 호출합니다.

초기설정

  • 데이터는 단일 데이터의 경우 해당 값을 String으로 전달. 다중 데이터는 JSON 형태의 String으로 전달한다.
javascript
// 네이티브 요청 함수 — UA 가 아니라 "주입된 객체의 형태"로 판별한다
function callNative(method, message) {
  var msg = message == null ? '' : String(message);
  var bridge = window.addedBridge;
  var ios = window.webkit && window.webkit.messageHandlers
    && window.webkit.messageHandlers.addedBridge;

  // 1) Android — @JavascriptInterface 로 메서드가 객체에 직접 주입된다.
  //    반드시 주입 객체의 메서드로 호출한다(함수를 변수에 꺼내 부르면 거절된다).
  if (bridge && typeof bridge[method] === 'function') {
    bridge[method](msg);
    return true;
  }

  // 2) Flutter — JavaScriptChannel 은 문자열 1개만 받는다.
  //    iOS 의 webview_flutter 는 window.addedBridge 를 messageHandler 와 "같은 객체"로 심으므로
  //    iOS 보다 먼저 검사해야 한다(객체를 보내면 Dart 에서 해석에 실패해 호출이 유실된다).
  if (bridge && typeof bridge.postMessage === 'function' && (!ios || bridge === ios)) {
    bridge.postMessage(JSON.stringify({ method: method, message: msg }));
    return true;
  }

  // 3) iOS — 단일 messageHandler 에 { method, message } 객체로 보낸다.
  if (ios && typeof ios.postMessage === 'function') {
    ios.postMessage({ method: method, message: msg });
    return true;
  }

  return false; // 브릿지 미주입(일반 브라우저)
}

callNative('interfaceName', 'value');

// 요청 데이터 받기 — 광고·백키 등 다른 기능도 같은 객체를 쓰므로 덮어쓰지 말고 병합한다
window.addedBridgeCallback = window.addedBridgeCallback || {};

addedBridgeCallback.interfaceName = function (data) {
  console.log('addedBridgeCallback :: ' + data);
};

// 콜백 payload 는 객체 또는 JSON 문자열로 올 수 있다 — 둘 다 받는다
function parsePayload(data) {
  if (data == null || data === '') return {};
  if (typeof data !== 'string') return data;
  try { return JSON.parse(data); } catch (e) { return {}; }
}

공통 규약

  • 플랫폼은 주입 형태로 판별합니다 — Android → Flutter → iOS 순서입니다. User-Agent 로 가르면 Flutter 호스트를 놓치고, iOS 를 Flutter 보다 먼저 검사하면 Flutter(iOS) 호스트에서 모든 호출이 유실됩니다.
  • Android 는 addedBridge[method](msg) 처럼 주입 객체의 메서드로호출합니다. 함수를 변수에 꺼내 부르면 "Java bridge method can't be invoked on a non-injected object" 로 거절됩니다.
  • window.addedBridgeCallback 은 광고·백키·조회 콜백이 함께 쓰는 객체입니다. 새로 만들지 말고 기존 객체에 병합합니다.
  • 콜백 payload 는 객체 또는 JSON 문자열로 올 수 있습니다(parsePayload).
  • 콜백에는 ACK 가 없습니다. 앱이 콜백을 보내지 않거나 유실해도 웹이 영원히 기다리지 않도록 응답을 기다리는 모든 경로에 상한을 둡니다. 권장값: 조회 3초 → null, 광고 로드 5초 → no-fill, 광고 노출 10초 → 미노출.
  • 조회 응답에는 requestId 가 없습니다. 같은 메서드를 연달아 호출하면 응답을 도착 순서대로 매칭합니다(아래 requestNative).
javascript
// 조회 헬퍼 — 응답에 requestId 가 없으므로 메서드별 대기열(FIFO)로 도착 순서대로 매칭하고,
// 응답을 보내지 않는 호스트(미구현 · 브라우저)에 대비해 상한을 둔다.
var pendingGetters = {};

function requestNative(method, timeoutMs) {
  return new Promise(function (resolve) {
    var queue = pendingGetters[method];
    if (!queue) {
      queue = pendingGetters[method] = [];
      addedBridgeCallback[method] = function (data) {
        var next = queue.shift();
        if (!next) return;               // 요청 없이 온 응답(중복 · 지연 도착)은 버린다
        clearTimeout(next.timer);
        next.resolve(data);
      };
    }
    var entry = { resolve: resolve };
    entry.timer = setTimeout(function () {
      var i = queue.indexOf(entry);
      if (i >= 0) queue.splice(i, 1);
      resolve(null);                     // 응답 없음
    }, timeoutMs || 3000);
    queue.push(entry);

    if (!callNative(method)) {           // 브릿지 미주입
      clearTimeout(entry.timer);
      queue.splice(queue.indexOf(entry), 1);
      resolve(null);
    }
  });
}

필수

openExternalBrowser()

  • 콘텐츠사에서 요청 받은 URL을 외부 브라우저로 연결합니다.
  • @param {string} url
javascript
callNative('openExternalBrowser', 'https://...');

finish()

  • 현재 웹뷰를 종료합니다.
  • 웹뷰를 벗어나는 경로는 항상 finish() 입니다. history.back() 으로 대신하지 않습니다 — 매체 앱이 처음 연 페이지는 돌아갈 히스토리가 없어 아무 일도 일어나지 않고, 광고 SDK 가 해시 엔트리를 쌓아 history.length 도 판단 근거가 되지 못합니다.
javascript
callNative('finish');

registerBackKey()

  • 시스템 뒤로가기(제스처/버튼)가 호출되면 네이티브에서 addedBridgeCallback.onBackKeyListener() 를 호출합니다.
  • @param {string} true|false
  • 등록하지 않으면 호스트가 웹뷰 뒤로가기 또는 종료를 직접 처리합니다. 등록했다면 종료까지 웹이 책임집니다(최상위에서 finish()).
  • pushUrl 등으로 문서가 바뀌면 새 페이지에서 콜백과 함께 다시 등록합니다.
javascript
// 콜백을 먼저 등록하고, 그다음 호스트에 등록을 알린다 — 문서(페이지)마다 1회
addedBridgeCallback.onBackKeyListener = function () {
  // 게임 안의 팝업 · 하위 화면은 웹이 직접 닫는다
  if (closeTopLayer()) return;
  callNative('finish'); // 필수연동 finish()
};
callNative('registerBackKey', 'true');

registerBackButton()

  • 앱바 영역에 백버튼이 존재하는 경우에만 필요합니다.
  • 앱바의 백버튼이 터치되면 네이티브에서 addedBridgeCallback.onBackButtonListener() 를 호출합니다.
  • @param {string} true|false
javascript
addedBridgeCallback.onBackButtonListener = function () {
  if (closeTopLayer()) return;
  callNative('finish'); // 필수연동 finish()
};
callNative('registerBackButton', 'true');

선택

조회 메서드는 응답이 오지 않으면(미구현 호스트) 상한 후 null 로 처리합니다.

getAppVersion()

  • 네이티브 앱의 현재 버전 정보를 조회합니다.
  • 응답은 addedBridgeCallback.getAppVersion(version) 으로 전달됩니다.
javascript
// 호출 → 콜백 addedBridgeCallback.getAppVersion(version)
const version = await requestNative('getAppVersion');
// ex) "3.12.0" — 응답이 없으면 null

getADID()

  • 디바이스 광고 식별값(ADID/IDFA)을 조회합니다.
  • 응답은 addedBridgeCallback.getADID(adId) 으로 전달됩니다.
javascript
// 호출 → 콜백 addedBridgeCallback.getADID(adId)
const adId = await requestNative('getADID');
// ex) "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
// iOS 는 추적 허용(ATT)을 받지 못하면 "00000000-0000-0000-0000-000000000000"

getIsAgeUnder14()

  • 유저의 나이가 14세 미만인지 여부를 조회합니다.
  • 구글/네이버 등 광고 정책으로 14세 미만에게는 타겟 광고 노출이 불가능합니다.
  • 응답은 addedBridgeCallback.getIsAgeUnder14(isUnder14)으로 전달됩니다. ("true" | "false", 알 수 없으면 빈 문자열)
javascript
// 호출 → 콜백 addedBridgeCallback.getIsAgeUnder14(isUnder14)
const raw = await requestNative('getIsAgeUnder14');
const isUnder14 = raw === 'true' ? true : raw === 'false' ? false : null; // 알 수 없으면 null

getDeviceInfo()

  • 기기 정보(network / model / manufacturer / osVersion)를 조회합니다.
  • 응답은 addedBridgeCallback.getDeviceInfo(deviceInfoJson) 으로 전달됩니다.
  • ex) {"network":"wifi","model":"SM-S911N","manufacturer":"samsung","osVersion":"14"}
javascript
// 호출 → 콜백 addedBridgeCallback.getDeviceInfo(deviceInfoJson)
const raw = await requestNative('getDeviceInfo');
const info = raw == null ? null : parsePayload(raw);
// ex) { network: "wifi", model: "SM-S911N", manufacturer: "samsung", osVersion: "14" }

pushUrl()

  • 현재 웹뷰를 재사용하여 URL을 변경합니다.
  • @param {string} JSON string { url, replace? }
  • 새 문서에는 이전 페이지의 콜백이 남지 않습니다. 새 페이지에서 콜백과 백키 등록을 다시 합니다.
javascript
callNative('pushUrl', JSON.stringify({
  url: 'https://...',
  replace: false
}));
// 새 문서가 열리면 그 페이지에서 콜백과 registerBackKey / registerBackButton 을 다시 등록한다

openInternalBrowser()

  • 새로운 앱 화면(Activity / UIViewController)에 웹뷰를 띄우는 인터페이스.
  • @param {string} url
javascript
callNative('openInternalBrowser', 'https://...');

setTitle()

  • 앱바 영역을 앱에서 관리하고, 해당 영역에 타이틀이 존재하는 경우 사용합니다.
  • 앱바 영역 타이틀을 수정합니다.
  • @param {string} title
javascript
callNative('setTitle', '제목');

광고 (보상형 · 전면형)

AdMob 보상형(rewarded) · 전면형(interstitial) 광고를 호출하는 인터페이스입니다. 두 형식 모두 전체 화면과 닫기 UI 를 AdMob SDK 가 소유하므로, 웹은 네이티브 오버레이와 달리 자리(rect)를 넘길 필요가 없습니다. 광고가 닫히면 onAdClosed, 보상 조건을 충족하면(보상형만) onAdRewarded 로 통지받습니다. 비동기 응답은 requestId 로 상관(correlate)합니다.

format표시 메서드콜백 순서보상
rewardedadShowRewardedonAdShown → onAdRewarded → onAdClosedonAdRewarded 수신 시
interstitialadShowonAdShown → onAdClosed없음
대부분 라이브러리가 자동 처리
콘텐츠는 보통 @blomics/added-mediation 또는 @blomics/added-bridge 의 bridge.ads 로 광고를 호출하며, 아래 브릿지 메서드 호출·콜백 등록·상한 처리는 라이브러리가 내부에서 수행합니다. 이 명세는 그 원시 인터페이스를 참고용으로 정리한 것입니다.

adInit()

  • 부팅 시 1회. 호스트가 받은 네트워크 SDK(AdMob 등)를 초기화합니다.
  • @param {string} JSON string { test, networks }
javascript
// 부팅 시 1회 — 호스트 광고 SDK 초기화
callNative('adInit', JSON.stringify({
  test: false,
  networks: [
    { key: 'admob', surface: 'bridge', app: { appId: 'ca-app-pub-XXXX~YYYY' } }
  ]
}));

adLoad()

  • 광고 1개를 로드합니다. format 으로 종류를 지정하며('rewarded' | 'interstitial'), 화면에 표시하지 않고 로드만 합니다.
  • 호스트가 지원하지 않는 형식은 즉시 onAdFailed(code:'UNSUPPORTED_FORMAT')로 돌아옵니다.
  • requestId 는 웹이 광고마다 고유하게 만듭니다. 이후 모든 콜백이 이 값을 싣고 옵니다.
  • ids.adUnitId 를 비우면 매체 앱의 기본 유닛이 쓰입니다.
  • 결과는 addedBridgeCallback.onAdLoaded / onAdFailed 로 전달됩니다.
  • @param {string} JSON string { requestId, network, format, ids }
javascript
// 광고 1개 로드 (아직 화면에 표시하지 않음)
callNative('adLoad', JSON.stringify({
  requestId: 'ad_1',
  network: 'admob',
  format: 'rewarded',                          // 'rewarded' | 'interstitial'
  ids: { adUnitId: 'ca-app-pub-XXXX/ZZZZ' }   // 비우면 매체 앱의 기본 유닛
}));

// 로드 결과 (호스트 → 웹) — 5초 안에 둘 다 오지 않으면 no-fill 로 처리한다
addedBridgeCallback.onAdLoaded = function (data) {
  const { requestId } = parsePayload(data);
  // fill 성공 → 이어서 형식에 맞는 표시 메서드 호출
};
addedBridgeCallback.onAdFailed = function (data) {
  const { requestId, stage, code } = parsePayload(data);
  // stage:'load' → 다음 후보로 / stage:'show' → 노출 실패로 종결
};

adShowRewarded() — 보상형

  • 로드된 보상형 광고를 표시합니다. 노출되면 onAdShown, 보상 조건을 충족하면 onAdRewarded, 광고가 닫히면 onAdClosed, 표시에 실패하면 onAdFailed(stage:'show')로 전달됩니다.
  • onAdRewarded 는 클라이언트 신호이므로 실제 보상 지급은 서버(S2S) 검증 후 처리해야 합니다.
  • @param {string} JSON string { requestId }

adShow() — 전면형

  • 로드된 전면형 광고를 표시합니다. 노출되면 onAdShown, 광고가 닫히면 onAdClosed, 표시에 실패하면 onAdFailed(stage:'show')로 전달됩니다.
  • 전면형에는 onAdRewarded 가 오지 않습니다(와도 무시합니다). onAdClosed 가 정상 종료이며 콘텐츠 흐름 재개 신호입니다.
  • 호스트는 adShow 와 adShowRewarded 를 같은 동작(로드한 광고를 그 형식대로 표시)으로 받습니다. 보상형을 adShow 로 불러도 됩니다.
  • @param {string} JSON string { requestId }
javascript
// 로드된 광고를 표시 — 보상형은 adShowRewarded, 전면형은 adShow.
// 결과 콜백은 형식과 무관하게 같은 이름으로 오므로 requestId 로 어느 광고인지 가른다.
const showing = {};   // requestId → { format, rewarded }

function showAd(requestId, format) {
  showing[requestId] = { format: format, rewarded: false };
  callNative(format === 'rewarded' ? 'adShowRewarded' : 'adShow', JSON.stringify({ requestId }));
}

addedBridgeCallback.onAdShown = function (data) { /* 노출(impression) */ };
addedBridgeCallback.onAdClicked = function (data) { /* 클릭 */ };
addedBridgeCallback.onAdRewarded = function (data) {
  const { requestId, type, amount } = parsePayload(data);
  const ad = showing[requestId];
  // 보상 조건 충족(예: 영상 끝까지 시청) — onAdClosed 보다 먼저 온다. 전면형에 온 신호는 무시한다.
  if (ad && ad.format === 'rewarded') ad.rewarded = true;
};
addedBridgeCallback.onAdClosed = function (data) {
  const { requestId } = parsePayload(data);
  const ad = showing[requestId];
  delete showing[requestId];
  if (!ad) return;
  // 광고 종료 = 노출 후 닫힘. 보상 여부는 이 시점까지 onAdRewarded 를 받았는지로 판정한다.
  // ⚠️ 클라이언트 신호일 뿐입니다 — 실제 보상 지급은 서버(S2S) 검증 후 처리하세요.
  if (ad.rewarded) requestServerReward();
  resumeContent();     // 전면형은 닫힘이 곧 종료 — 보상 없이 흐름만 재개한다
};

adDestroy() — 선택

  • 로드만 하고 표시하지 않을 광고를 정리합니다. 표시한 광고는 SDK 가 1회성으로 소비하므로 부를 필요가 없습니다.
  • @param {string} JSON string { requestId }
javascript
// 로드만 하고 표시하지 않을 광고 정리
callNative('adDestroy', JSON.stringify({ requestId: 'ad_1' }));
광고 결과 판정 규칙
  • onAdClosed 도달 자체를 노출로 봅니다 — onAdShown 을 보내지 않는 호스트가 있습니다.
  • onAdClosed 까지 onAdRewarded 가 없었으면 미보상(스킵·중도 이탈)입니다.
  • 전면형은 보상 판정이 없습니다. onAdRewarded 가 와도 무시하고 onAdClosed 도달을 정상 종료로 봅니다.
  • 표시 메서드 호출 후 10초 안에 onAdShown · onAdFailed · onAdClosed 중 아무것도 오지 않으면 미노출로 종결합니다. 광고가 로드까지 되고 노출만 안 된다면 adShow 만 구현한 구형 호스트일 가능성을 먼저 확인하세요. Android 는 웹이 메서드 존재를 확인할 수 있지만 iOS · Flutter 는 단일 채널이라 확인할 수 없습니다 — @blomics/added-bridge 는 이때 adShowRewardedMethod: 'adShow' 옵션으로 호출 이름을 고정합니다.

관련 문서