개발 가이드
웹뷰 연동
웹뷰가 네이티브앱에서 정상적으로 동작하기 위한 웹뷰 설정입니다. 적용되지 않은 경우 뒤로가기, 광고 노출 등이 정상 작동하지 않을 수 있습니다. 매체사와 콘텐츠 제작사 모두 이 문서를 참고하여 연동해야 합니다.
kotlin
val settings = webView.settings
settings.mediaPlaybackRequiresUserGesture = falseiOS
swift
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true// 영상이 자동으로 전체화면이 되지 않도록 설정
config.mediaTypesRequiringUserActionForPlayback = []// 사용자 제스처 없이 자동재생 허용 (오디오/비디오 모두 적용)
// iOS 9버전 이하일시 ===========================
config.mediaPlaybackRequiresUserAction = falseFlutter
dart
InAppWebView(
initialSettings: InAppWebViewSettings(
//공통: 사용자 제스처 없이 자동재생 허용
mediaPlaybackRequiresUserGesture: false,
//IOS ===================================
allowsInlineMediaPlayback: true,//영상이 자동으로 전체화면이 되지않도록 설정
allowsPictureInPictureMediaPlayback: false,// PIP(Picture-in-Picture) 비활성화
),
)생명주기
앱이 백그라운드 진입 시 영상을 자동으로 중단하기 위해 설정합니다.
AOS
kotlin
override fun onResume() {
super.onResume()
webView.onResume()
WebView.resumeTimers()
}
override fun onPause() {
super.onPause()
webView.onPause()
WebView.pauseTimers()
}iOS
swift
//해당없음Flutter
dart
lass _WebViewPageState extends State<WebViewPage> with WidgetsBindingObserver {
InAppWebViewController? _controller;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!Platform.isAndroid) return;
if (state == AppLifecycleState.paused) {
_controller?.pauseTimers();
} else if (state == AppLifecycleState.resumed) {
_controller?.resumeTimers();
}
}
// ...
}URL 처리
광고 랜딩 URL은 세션/스토리지 오염 방지, 광고 시청 확인 여부, 딥링크 연동(앱열기), UX 보존(뒤로가기 등)과 같은 이유로 항상 외부 브라우저로 열려야 합니다.
- 만약 내부에서 동작되어야 할 도메인들은 허용 도메인(
allowedDomains)에 추가해 예외 처리 합니다. *.blomics.net,*.tenqube.com도메인은 허용 도메인에 반드시 추가되어야 합니다.- 아래는 예제 코드입니다.
네이티브 앱 정책 확인 필요
해당 설정의 경우 네이티브 앱의 정책에 따라 다양한 케이스가 존재하므로 내부 정책 및 설정을 확인하시기 바랍니다.
- URL을 외부 브라우저로 여는 경우
- 비허용 도메인 링크 클릭, 새 창 시도, 네비게이션 등
- 비 http/https 스킴(mailto:, tel:, sms:, intent:, itms-apps: 등)
- 내부 웹뷰 재사용 하는 경우
- 초기 로드 URL
- 허용 도메인 링크 클릭, 새 창 시도, 네비게이션 등
AOS
kotlin
// 허용 도메인(서브도메인 포함) — 내부에서 열릴 도메인 추가
private val allowedDomains = setOf(
"*.blomics.net"
)
private fun isAllowedDomain(host: String?): Boolean {
val h = host?.lowercase() ?: return false
return allowedDomains.any { base -> h == base || h.endsWith(".$base") }
}
private fun shouldOpenExternally(url: Uri): Boolean {
val scheme = url.scheme?.lowercase() ?: return true
return if (scheme == "http" || scheme == "https") {
!isAllowedDomain(url.host)
} else {
true
}
}
private fun openExternally(uri: Uri) {
try {
startActivity(Intent(Intent.ACTION_VIEW, uri))
} catch (_: ActivityNotFoundException) {}
}
private fun tryHandleSpecialSchemes(uri: Uri): Boolean {
val scheme = (uri.scheme ?: "").lowercase()
if (scheme == "intent") {
try {
val intent = Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME)
try {
startActivity(intent)
return true
} catch (_: ActivityNotFoundException) {
intent.`package`?.let { pkg ->
val market = Uri.parse("market://details?id=$pkg")
try { startActivity(Intent(Intent.ACTION_VIEW, market)) } catch (_: Exception) {}
}
return true
}
} catch (_: Exception) { return true }
}
if (scheme == "market") {
openExternally(uri)
return true
}
return false
}iOS
swift
// 허용 도메인(서브도메인 포함) — 내부에서 열릴 도메인 추가
private let allowedDomains: Set<String> = [
"*.blomics.net"
]
private func isAllowedDomain(_ host: String?) -> Bool {
guard let h = host?.lowercased() else { return false }
return allowedDomains.contains(where: { base in h == base || h.hasSuffix("." + base) })
}
// 허용되지 않은 http(s) 또는 비 http(s) 스킴이면 true
private func shouldOpenExternally(_ url: URL) -> Bool {
guard let scheme = url.scheme?.lowercased() else { return true }
if scheme == "http" || scheme == "https" {
return !isAllowedDomain(url.host)
}
// mailto:, tel:, sms:, itms-apps: 등 비 http(s) 스킴은 외부
return true
}
private func openExternally(_ url: URL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
//<a> 링크 클릭 정책 처리 (isUserClick만 담당, isNewWindow는 createWebViewWith에서 처리)
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow); return
}
let isUserClick = navigationAction.navigationType == .linkActivated
if isUserClick && shouldOpenExternally(url) {
decisionHandler(.cancel)
openExternally(url)
} else {
decisionHandler(.allow)
}
}
// window.open / target="_blank" 새 창 생성 정책 처리 (isNewWindow 담당)
func webView(_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures) -> WKWebView? {
guard let url = navigationAction.request.url else { return nil }
if shouldOpenExternally(url) {
openExternally(url)
} else {
webView.load(URLRequest(url: url))
}
return nil
}Flutter
dart
// 허용 도메인(서브도메인 포함) — 내부에서 열릴 도메인 추가
final Set<String> allowedDomains = {
"*.blomics.net"
};
bool isAllowedDomain(String? host) {
final h = host?.toLowerCase();
if (h == null) return false;
for (final base in allowedDomains) {
if (h == base || h.endsWith(".$base")) return true;
}
return false;
}
// 허용되지 않은 http(s) 또는 비 http(s) 스킴이면 true
bool shouldOpenExternally(Uri url) {
final scheme = url.scheme.toLowerCase();
if (scheme == "http" || scheme == "https") {
return !isAllowedDomain(url.host);
}
// mailto:, tel:, sms:, itms-apps: 등 비 http(s) 스킴은 외부
return true;
}
Future<void> openExternally(Uri url) async {
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
}샘플 코드
AOS
kotlin
package com.tenqube.alloweddomainwebview
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.os.Message
import android.webkit.*
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
// 허용 도메인(서브도메인 포함) — 내부에서 열릴 도메인 추가
private val allowedDomains = setOf(
"*.blomics.net"
)
private fun isAllowedDomain(host: String?): Boolean {
val h = host?.lowercase() ?: return false
return allowedDomains.any { base -> h == base || h.endsWith(".$base") }
}
private fun shouldOpenExternally(url: Uri): Boolean {
val scheme = url.scheme?.lowercase() ?: return true
return if (scheme == "http" || scheme == "https") {
!isAllowedDomain(url.host)
} else {
true // 비 http(s) 스킴은 외부로
}
}
private fun openExternally(uri: Uri) {
try {
startActivity(Intent(Intent.ACTION_VIEW, uri))
} catch (_: ActivityNotFoundException) {}
}
private fun tryHandleSpecialSchemes(uri: Uri): Boolean {
val scheme = (uri.scheme ?: "").lowercase()
if (scheme == "intent") {
try {
val intent = Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME)
try {
startActivity(intent)
return true
} catch (_: ActivityNotFoundException) {
intent.`package`?.let { pkg ->
val market = Uri.parse("market://details?id=$pkg")
try { startActivity(Intent(Intent.ACTION_VIEW, market)) } catch (_: Exception) {}
}
return true
}
} catch (_: Exception) { return true }
}
if (scheme == "market") {
openExternally(uri)
return true
}
return false
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
// 쿠키 허용 (광고 타겟팅 및 세션 유지에 필요)
CookieManager.getInstance().setAcceptCookie(true)
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
val settings = webView.settings
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.setSupportMultipleWindows(true) // window.open 허용
settings.javaScriptCanOpenWindowsAutomatically = true
settings.userAgentString = settings.userAgentString + " AllowedDomainWebView/1.0"
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
// 영상 자동재생 허용 (사용자 제스처 불필요)
settings.mediaPlaybackRequiresUserGesture = false
override fun onPermissionRequest(request: PermissionRequest?) {
request?.grant(request.resources)
}
// window.open / target=_blank 처리
override fun onCreateWindow(
view: WebView?, isDialog: Boolean,
isUserGesture: Boolean, resultMsg: Message?
): Boolean {
val temp = WebView(this@MainActivity).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.setSupportMultipleWindows(true)
settings.javaScriptCanOpenWindowsAutomatically = true
webViewClient = object : WebViewClient() {
override fun onPageStarted(v: WebView?, urlStr: String?, favicon: Bitmap?) {
val target = urlStr ?: "about:blank"
val uri = Uri.parse(target)
if (tryHandleSpecialSchemes(uri)) { try { v?.destroy() } catch (_: Throwable) {}; return }
if (shouldOpenExternally(uri)) openExternally(uri)
else this@MainActivity.webView.loadUrl(target)
try { v?.destroy() } catch (_: Throwable) {}
}
}
}
val transport = resultMsg?.obj as? WebView.WebViewTransport ?: return false
transport.webView = temp
resultMsg.sendToTarget()
return true
}
override fun onCloseWindow(window: WebView?) {
try { window?.destroy() } catch (_: Throwable) {}
super.onCloseWindow(window)
}
}
// 뒤로가기 처리
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (this@MainActivity::webView.isInitialized && webView.canGoBack()) {
webView.goBack()
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
})
}
}iOS
swift
import UIKit
import WebKit
final class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
private lazy var webView: WKWebView = {
let config = WKWebViewConfiguration()
// window.open 허용
config.preferences.javaScriptCanOpenWindowsAutomatically = true
// 영상 광고 인라인 재생 허용
config.allowsInlineMediaPlayback = true
// 사용자 제스처 없이 자동재생 허용 (오디오·비디오 모두)
config.mediaTypesRequiringUserActionForPlayback = []
let wv = WKWebView(frame: .zero, configuration: config)
wv.navigationDelegate = self
wv.uiDelegate = self
wv.translatesAutoresizingMaskIntoConstraints = false
wv.allowsBackForwardNavigationGestures = true
return wv
}()
// 허용 도메인(서브도메인 포함) — 내부에서 열릴 도메인 추가
private let allowedDomains: Set<String> = [
" *.blomics.net"
]
override func viewDidLoad() {
super.viewDidLoad()
// 서드파티 쿠키 허용 (광고 타겟팅 및 세션 유지에 필요)
// WKWebView는 기본적으로 HTTPCookieStorage.shared를 사용하며,
// iOS 11+ 에서는 WKHTTPCookieStore를 통해 쿠키를 직접 제어할 수 있습니다.
HTTPCookieStorage.shared.cookieAcceptPolicy = .always
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
private func isAllowedDomain(_ host: String?) -> Bool {
guard let h = host?.lowercased() else { return false }
return allowedDomains.contains(where: { base in h == base || h.hasSuffix("." + base) })
}
// 허용되지 않은 http(s) 또는 비 http(s) 스킴이면 true
private func shouldOpenExternally(_ url: URL) -> Bool {
guard let scheme = url.scheme?.lowercased() else { return true }
if scheme == "http" || scheme == "https" {
return !isAllowedDomain(url.host)
}
// mailto:, tel:, sms:, itms-apps: 등 비 http(s) 스킴은 외부
return true
}
private func openExternally(_ url: URL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
// <a> 링크 클릭 정책 처리 (isUserClick만 담당, isNewWindow는 createWebViewWith에서 처리)
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow); return
}
let isUserClick = navigationAction.navigationType == .linkActivated
if isUserClick && shouldOpenExternally(url) {
decisionHandler(.cancel)
openExternally(url)
} else {
decisionHandler(.allow)
}
}
// window.open / target="_blank" 새 창 생성 정책 처리 (isNewWindow 담당)
func webView(_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures) -> WKWebView? {
guard let url = navigationAction.request.url else { return nil }
if shouldOpenExternally(url) {
openExternally(url)
} else {
webView.load(URLRequest(url: url))
}
return nil
}
}Flutter
dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isAndroid) {
await InAppWebViewController.setWebContentsDebuggingEnabled(true);
}
// iOS 서드파티 쿠키 허용 (광고 타겟팅 및 세션 유지에 필요)
// Android는 InAppWebViewSettings.thirdPartyCookiesEnabled로 처리
if (Platform.isIOS) {
final cookieManager = CookieManager.instance();
await cookieManager.setCookiesEnabled(true);
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: WebViewPage(),
);
}
}
class WebViewPage extends StatefulWidget {
const WebViewPage({super.key});
@override
State<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
InAppWebViewController? _controller;
// 허용 도메인(서브도메인 포함) — 내부에서 열릴 도메인 추가
final Set<String> allowedDomains = {
"*.blomics.net",
};
bool isAllowedDomain(String? host) {
final h = host?.toLowerCase();
if (h == null) return false;
for (final base in allowedDomains) {
if (h == base || h.endsWith(".$base")) return true;
}
return false;
}
// 허용되지 않은 http(s) 또는 비 http(s) 스킴이면 true
bool shouldOpenExternally(Uri url) {
final scheme = url.scheme.toLowerCase();
if (scheme == "http" || scheme == "https") {
return !isAllowedDomain(url.host);
}
// mailto:, tel:, sms:, itms-apps: 등 비 http(s) 스킴은 외부
return true;
}
Future<void> openExternally(Uri url) async {
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri(""),
),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
useShouldOverrideUrlLoading: true,
allowsBackForwardNavigationGestures: true,
javaScriptCanOpenWindowsAutomatically: true,
supportMultipleWindows: true,
//영상 자동재생 허용
mediaPlaybackRequiresUserGesture: false,
//AOS ====================================
thirdPartyCookiesEnabled: true,//서드파티 쿠키 허용
//IOS =====================================
allowsInlineMediaPlayback: true,//인라인 재생 허용
allowsPictureInPictureMediaPlayback: false,
),
onWebViewCreated: (controller) {
_controller = controller;
},
// <a> 링크 클릭 처리
shouldOverrideUrlLoading: (controller, navigationAction) async {
final uri = navigationAction.request.url;
if (uri == null) return NavigationActionPolicy.ALLOW;
final isUserClick = navigationAction.navigationType == NavigationType.LINK_ACTIVATED;
if (isUserClick && shouldOpenExternally(Uri.parse(uri.toString()))) {
await openExternally(Uri.parse(uri.toString()));
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
// window.open / target="_blank" 처리
onCreateWindow: (controller, onCreateWindowRequest) async {
final uri = onCreateWindowRequest.request.url;
if (uri == null) return true;
if (shouldOpenExternally(Uri.parse(uri.toString()))) {
await openExternally(Uri.parse(uri.toString()));
} else {
// 새 창을 만들지 않고 현재 웹뷰에서 로드 (재사용)
await _controller?.loadUrl(urlRequest: URLRequest(url: uri));
}
return false;
},
),
),
);
}
}자바스크립트 인터페이스
초기설정
- 데이터는 단일 데이터의 경우 해당 값을
String으로 전달하며, 다중 데이터는JSON형태의 String으로 전달합니다. 자세한 가이드는 각 상세 가이드를 참고해주세요.
매체사(WebView → Web)
finish()— 현재 웹뷰를 종료합니다.registerBackKey()— 시스템 뒤로가기(제스처/버튼)가 호출되면 네이티브에서addedBridgeCallback.onBackKeyListener()를 호출합니다.registerBackButton()— 앱바 영역에 백버튼이 존재하는 경우, 백버튼 터치 시 네이티브에서addedBridgeCallback.onBackButtonListener()를 호출합니다.
자세한 사용법은 매체사 상세 가이드를 참고하세요.
콘텐츠사(Web → WebView)
openExternalBrowser()— 콘텐츠사에서 요청 받은 URL을 외부 브라우저로 연결합니다.finish()— 현재 웹뷰를 종료합니다.registerBackKey()— 시스템 뒤로가기(제스처/버튼) 발생 시 호출될 콜백을 등록합니다.registerBackButton()— 앱바 백버튼이 있는 경우, 백버튼 터치 시 호출될 콜백을 등록합니다.
자세한 사용법은 콘텐츠사 상세 가이드를 참고하세요.
웹뷰 최적화 설정
광고 수익화를 위한 추가 권장 설정 (Google for Developers).
웹뷰 디버깅
개발 단계에서는 Chrome DevTools
chrome://inspect 또는 Safari Web Inspector 로 웹뷰를 디버깅할 수 있도록 setWebContentsDebuggingEnabled(true) (Android) / isInspectable = true (iOS 16.4+) 를 활성화하세요. 프로덕션 빌드에선 비활성화 필수.