Guides

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 설정

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.xmlWebView를 추가합니다.

 <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 설정

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()
                // 오류 처리
            }
        }
    }

   ...
}