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. 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":   // 배너 클릭시 message.body를 통해 bannerId (배너 고유 아이디), linkUrl (배너에 등록된 URL), broadcastIdx (방송 번호) 정보를 확인할 수 있습니다.
                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 {
             // 처리 코드를 여기에 추가
        }
    }    
 
}