Web + Mobile 통신 구현 (Bridge)
웹과 모바일의 "Bridge(브릿지)"는 특히 하이브리드 앱이나 네이티브 모듈을 포함한 웹 애플리케이션에서 웹 콘텐츠와 네이티브 코드 사이의 통신을 도와주는 연결 코드를 지칭합니다.
이러한 Bridge를 사용하면 웹 기술로 작성된 코드가 모바일 기기의 네이티브 기능, 예를 들면 카메라, GPS, 파일 시스템 등에 접근하거나 이를 조작할 수 있게 됩니다. 이를 통해 웹 기술을 활용하여 플랫폼 간 코드 재사용성을 높이면서 네이티브와 같은 성능과 사용자 경험을 제공하는 앱을 구축할 수 있습니다.
1. iOS Bridge 설정
프로젝트 설정
STEP 1. Xcode에서 새로운 프로젝트를 생성합니다.
STEP 2. WKWebView를 생성합니다.
웹 페이지 생성
간단한 웹 페이지 (index.html
)를 만들어서 프로젝트에 추가합니다. 예제로 다음과 같은 웹 페이지를 사용합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Bridge Example</title>
</head>
<body>
<button onclick="sendMessageToiOS('Hello from Web!')">Send Message to iOS</button>
<script>
function sendMessageToiOS(message) {
window.webkit.messageHandlers.bridge.postMessage(message);
}
</script>
</body>
</html>
WKWebView
설정
WKWebView
설정ViewController.swift
에서 WKWebView
를 설정하여 웹 페이지를 로드하고 JavaScript와의 통신을 준비합니다.
import UIKit
import WebKit
class ViewController: UIViewController, WKScriptMessageHandler {
@IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
contentController.add(self, name: "bridge")
let config = WKWebViewConfiguration()
config.userContentController = contentController
webView.configuration.userContentController = contentController
if let url = Bundle.main.url(forResource: "index", withExtension: "html") {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "bridge", let messageBody = message.body as? String {
print("Received message from web: \(messageBody)")
// 필요한 작업 수행
}
}
}
웹에서 iOS로 메시지 보내기
웹 페이지에 있는 버튼을 클릭하면, JavaScript 함수 sendMessageToiOS()
를 호출하여 iOS App으로 메시지를 보냅니다. iOS 측에서는 이 메시지를 userContentController(_:didReceive:)
메서드를 통해 받게 됩니다.
iOS에서 웹으로 메시지 보내기
iOS 앱에서 웹 페이지의 JavaScript 함수를 호출하려면 evaluateJavaScript(_:completionHandler:)
메서드를 사용합니다.
webView.evaluateJavaScript("alert('Hello from iOS!')", completionHandler: nil)
// PIP 사용 true 설정시 PIP 버튼 노출
let name = "(function() { window.dispatchEvent(sauceflexPictureInPictureUse(true)); })();"
// PIP 사용 false 설정시 PIP 버튼 미노출
let name = "(function() { window.dispatchEvent(sauceflexPictureInPictureUse(false)); })();"
webView?.evaluateJavaScript(name) { (Result, Error) in
if let error = Error {
print("evaluateJavaScript Error : \(error)")
}
print("evaluateJavaScript Result : \(Result ?? "success")")
}
2. Android Bridge 설정
프로젝트 설정
STEP 1. Android Studio를 사용하여 새로운 프로젝트를 생성합니다
STEP 2. AndroidManifest.xml에 Bridge사용을 위한 권한을 추가합니다.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
STEP 3. activity_main.xml
에 WebView
를 추가합니다.
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
웹페이지 생성
프로젝트의 assets
폴더에 index.html
파일을 생성합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Bridge Example</title>
</head>
<body>
<button onclick="sendMessageToAndroid()">Send Message to Android</button>
<script>
function sendMessageToAndroid() {
AndroidBridge.showMessage("Hello from Web!");
}
</script>
</body>
</html>
WebView
설정
WebView
설정MainActivity.java
또는 MainActivity.kt
에서 WebView를 설정합니다.
class MainActivity extends AppCompatActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.webview);
// JavaScript 활성화
webView.getSettings().setJavaScriptEnabled(true);
// JavaScript 인터페이스 추가
webView.addJavascriptInterface(new WebAppInterface(this), "sauceflex");
// Asset 폴더의 HTML 파일 로드
webView.loadUrl("file:///android_asset/index.html");
}
public class WebAppInterface {
Context mContext;
WebAppInterface(Context c) {
mContext = c;
}
@JavascriptInterface
public void showMessage(String message) {
// 처리 코드를 여기에 추가
}
}
}
MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webview)
// JavaScript 활성화
webView.settings.javaScriptEnabled = true
// JavaScript 인터페이스 추가
webView.addJavascriptInterface(WebAppInterface(this), "sauceflex")
// Asset 폴더의 HTML 파일 로드
webView.loadUrl("file:///android_asset/index.html")
}
class WebAppInterface(private val mContext: Context) {
@JavascriptInterface
fun showMessage(message: String) {
// 처리 코드를 여기에 추가
}
}
}
웹에서 Android로 메시지 보내기
웹 페이지에서 버튼을 클릭하면, sendMessageToAndroid()
JavaScript 함수를 통해 Android의 showMessage()
메서드가 호출됩니다.
Android에서 Web으로 메시지 보내기
WebView
를 통해 웹 페이지의 JavaScript 함수를 호출하려면 evaluateJavascript()
메서드를 사용합니다.
webView.evaluateJavascript("alert('Hello from Android!');", null)
// PIP 사용 true 설정시 PIP 버튼 노출
myWebView?.evaluateJavascript(
"(function() { window.dispatchEvent(sauceflexPictureInPictureUse(true)); })();"
) { }
// PIP 사용 false 설정시 PIP 버튼 미노출
myWebView?.evaluateJavascript(
"(function() { window.dispatchEvent(sauceflexPictureInPictureUse(false)); })();"
) { }
3. Flutter Bridge 설정
webview_flutter 패키지를 사용하여 Flutter 애플리케이션과 웹 콘텐츠 간에 통신하는 방법을 설명합니다.
필요한 패키지 추가
프로젝트의 pubspec.yaml 파일에 webview_flutter 패키지를 추가합니다.
dependencies:
flutter:
sdk: flutter
webview_flutter: ^3.0.0
WebView 위젯 설정
Flutter에서 WebView를 구현하여 웹 페이지를 로드하고 JavaScript와 통신을 설정합니다.
// code example
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewExample extends StatefulWidget {
@override
_WebViewExampleState createState() => _WebViewExampleState();
}
class _WebViewExampleState extends State<WebViewExample> {
final _controller = Completer<WebViewController>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('WebView Example'),
),
body: WebView(
initialUrl: 'https://yourwebsite.com',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
javascriptChannels: <JavascriptChannel>{
_createJavascriptChannel(),
},
),
);
}
JavascriptChannel _createJavascriptChannel() {
return JavascriptChannel(
name: 'FlutterBridge',
onMessageReceived: (JavascriptMessage message) {
print('Message received from web: ${message.message}');
});
}
}
웹 페이지에서 Flutter로 메시지 보내기
Flutter와 웹 콘텐츠 간의 기본적인 통신 방법을 설명합니다.
// code example
function sendMessageToFlutter(message) {
if (window.FlutterBridge) {
window.FlutterBridge.postMessage(message);
}
}
Flutter에서 웹 페이지로 메시지 보내기
필요에 따라 JavaScript와 Dart 코드를 조정하여 다양한 유형의 데이터를 교환할 수 있습니다
// code example
void sendMessageToWeb(WebViewController controller) async {
final url = await controller.currentUrl();
controller.evaluateJavascript("alert('This is a message from Flutter!')");
}
4. Player Bridge 연동 가이드 (웹 → 앱 통신)
기본 가이드대로 코딩 후 플랫폼 별로 코드를 추가하여 적용해 주세요.
플레이어 Bridge 추가
iOS
...
let contentController = WKUserContentController()
contentController.add(self, name: "sauceflexEnter")
contentController.add(self, name: "sauceflexMoveExit")
contentController.add(self, name: "sauceflexMoveLogin")
contentController.add(self, name: "sauceflexMoveProduct")
contentController.add(self, name: "sauceflexMoveBanner")
contentController.add(self, name: "sauceflexOnShare")
contentController.add(self, name: "sauceflexPictureInPicture")
contentController.add(self, name: "sauceflexWebviewReloading")
contentController.add(self, name: "sauceflexMoveReward")
...
Android
...
webView.addJavascriptInterface(new WebAppInterface(this), "sauceflex");
...
...
webView.addJavascriptInterface(WebAppInterface(this), "sauceflex")
...
통신 되는 Bridge 기능 추가
iOS
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
switch (message.name) {
case "sauceflexEnter": // 처음 플레이어 진입시
print("sauceflexEnter")
break
case "sauceflexMoveExit": // 닫기 버튼 눌러 팝업에서 나가기 클릭시
print("sauceflexMoveExit")
break
case "sauceflexMoveLogin": // 로그인 팝업에서 확인 클릭시
print("sauceflexMoveLogin")
break
case "sauceflexMoveProduct": // 상품 클릭시
print("sauceflexMoveProduct")
print(message.body)
break
case "sauceflexOnShare": // 공유하기 버튼 클릭시
print("sauceflexOnShare")
print(message.body)
break
case "sauceflexPictureInPicture": // PIP 전환 버튼 클릭시, PIP 보기 전환시 보여주고 있는 컴포넌트 GONE 처리를 자동으로 처리합니다. (좋아요 버튼 등 플레이어 위에 보여지는 버튼들)
print("sauceflexPictureInPicture")
print(message.body)
break
case "sauceflexMoveBanner": // 배너 클릭시
print("sauceflexMoveBanner")
print(message.body)
break
case "sauceflexWebviewReloading": // 웹뷰 리로딩
print("sauceflexWebviewReloading")
break
case "sauceflexMoveReward": // 리워드 완료시의 달성 버튼 클릭시
print("sauceflexMoveReward")
print(message.body)
break
default:
print("message.name \\(message.name) not handled.")
}
}
}
Android
public class AndroidBridge {
private final Context context;
private final Handler handler = new Handler();
public AndroidBridge(Context context) {
this.context = context;
}
// 처음 플레이어 진입시
@JavascriptInterface
public void sauceflexEnter() {
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
// 닫기 버튼 눌러 팝업에서 나가기시
@JavascriptInterface
public void sauceflexMoveExit() {
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
// 로그인 팝업에서 확인시
@JavascriptInterface
public void sauceflexMoveLogin() {
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
// 상품 클릭시
@JavascriptInterface
public void sauceflexMoveProduct(String message) {
final String finalMessage = message;
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
// 공유하기
@JavascriptInterface
public void sauceflexOnShare(String message) {
final String finalMessage = message;
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
// PIP 전환 버튼 클릭시, PIP 보기 전환시, 보여주고 있는 컴포넌트 GONE 처리를 자동으로 처리 합니다. (GONE 내용 ex: 좋아요 및 각정 플레이어 위에 띄워지는 버튼들)
@JavascriptInterface
public void sauceflexPictureInPicture() {
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
// 배너 클릭시 bannerId (배너 고유 아이디), linkUrl (배너에 등록된 URL), broadcastIdx (방송 번호)
@JavascriptInterface
public void sauceflexMoveBanner(String message) {
final String finalMessage = message;
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
// 웹뷰 리로딩
@JavascriptInterface
public void sauceflexWebviewReloading() {
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
// 리워드 기능
@JavascriptInterface
public void sauceflexMoveReward(String message) {
final String finalMessage = message;
handler.post(new Runnable() {
@Override
public void run() {
// 처리 코드를 여기에 추가
}
});
}
}
class AndroidBridge(private val context: Context) {
private val handler = Handler()
@JavascriptInterface // 처음 플레이어 진입시
fun sauceflexEnter() {
handler.post {
// 처리 코드를 여기에 추가
}
}
@JavascriptInterface // 닫기 버튼 눌러 팝업에서 나가기시
fun sauceflexMoveExit() {
handler.post {
// 처리 코드를 여기에 추가
}
}
@JavascriptInterface // 로그인 팝업에서 확인시
fun sauceflexMoveLogin() {
handler.post {
// 처리 코드를 여기에 추가
}
}
@JavascriptInterface // 상품 클릭시
fun sauceflexMoveProduct(message: String) {
handler.post {
// 처리 코드를 여기에 추가
}
}
@JavascriptInterface // 공유하기
fun sauceflexOnShare(message: String) {
handler.post {
// 처리 코드를 여기에 추가
}
}
@JavascriptInterface // PIP 전환 버튼 클릭시, PIP 보기 전환시, 보여주고 있는 컴포넌트 GONE 처리를 자동으로 처리 합니다. (GONE 내용 ex: 좋아요 및 각정 플레이어 위에 띄워지는 버튼들)
fun sauceflexPictureInPicture () {
handler.post {
// 처리 코드를 여기에 추가
}
}
@JavascriptInterface // 배너 클릭시 bannerId (배너 고유 아이디), linkUrl (배너에 등록된 URL), broadcastIdx (방송 번호)
fun sauceflexMoveBanner(message: String) {
handler.post {
// 처리 코드를 여기에 추가
}
}
@JavascriptInterface // 웹뷰 리로딩
fun sauceflexWebviewReloading() {
handler.post {
// 처리 코드를 여기에 추가
}
}
@JavascriptInterface // 리워드 기능
fun sauceflexMoveReward(message: String) {
handler.post {
// 처리 코드를 여기에 추가
}
}
}
5. Player Bridge 연동 예제 (공유하기)
iOS
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
switch (message.name) {
...
case "sauceflexOnShare": // 공유하기 버튼 클릭시
print(message.body)
// message.body가 URL 문자열이라고 가정합니다
guard let urlString = message.body as? String, let url = URL(string: urlString) else {
print("오류: 메시지 본문에 잘못된 URL이 포함되어 있습니다")
return
}
// 공유할 항목 준비
var objectsToShare = [url] // URL을 포함시킵니다
// 활동 뷰 컨트롤러 생성 및 표시
let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
activityVC.popoverPresentationController?.sourceView = self.view
// 특정 활동 제외가 필요할 경우 사용
// activityVC.excludedActivityTypes = [UIActivityType.airDrop, UIActivityType.addToReadingList]
self.present(activityVC, animated: true, completion: nil)
...
}
}
}
Android
public class AndroidBridge {
private Context context;
private Handler handler;
// Constructor to initialize the context and handler
public AndroidBridge(Context context) {
this.context = context;
this.handler = new Handler();
}
...
// 공유하기
@JavascriptInterface
public void sauceflexOnShare(String message) {
final String finalMessage = message;
handler.post(new Runnable() {
@Override
public void run() {
try {
// 메시지가 URL 문자열이라고 가정합니다
Uri uri = Uri.parse(message);
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, uri.toString());
context.startActivity(Intent.createChooser(intent, "Share via"));
} catch (Exception e) {
e.printStackTrace();
// 오류 처리
}
}
});
}
...
}
class AndroidBridge(private val context: Context) {
private val handler = Handler()
...
@JavascriptInterface // 공유하기
fun sauceflexOnShare(message: String) {
handler.post {
try {
// 메시지가 URL 문자열이라고 가정합니다
val uri = Uri.parse(message)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, uri.toString())
}
context.startActivity(Intent.createChooser(intent, "Share via"))
} catch (e: Exception) {
e.printStackTrace()
// 오류 처리
}
}
}
...
}
Updated about 1 month ago