TWA를 위한 PostMessage

Sayed El-Abady
Sayed El-Abady

Chrome 115부터 신뢰할 수 있는 웹 활동 (TWA)은 postMessage를 사용하여 메시지를 보낼 수 있습니다. 이 문서에서는 앱과 웹 간에 통신하는 데 필요한 설정을 설명합니다.

이 가이드를 완료하면 다음을 수행할 수 있습니다.

  • 클라이언트 및 웹 콘텐츠 유효성 검사의 작동 방식을 이해합니다.
  • 클라이언트와 웹 콘텐츠 간의 통신 채널을 초기화하는 방법을 알고 있어야 합니다.
  • 웹 콘텐츠에 메시지를 보내고 웹 콘텐츠에서 메시지를 받는 방법을 알고 있어야 합니다.

이 가이드를 따르려면 다음이 필요합니다.

  • build.gradle 파일에 최신 androidx.browser (최소 v1.6.0-alpha02) 라이브러리를 추가합니다.
  • TWA의 경우 Chrome 버전 115.0.5790.13 이상

window.postMessage() 메서드는 Window 객체 간의 교차 출처 통신을 안전하게 사용 설정합니다. 예를 들어 페이지와 해당 페이지에서 생성된 팝업 사이 또는 페이지와 페이지 내에 삽입된 iframe 사이의 연결이 여기에 해당합니다.

일반적으로 서로 다른 페이지의 스크립트는 페이지가 동일한 출처에서 비롯되고 동일한 프로토콜, 포트 번호, 호스트를 공유하는 경우에만 서로 액세스할 수 있습니다 (동일 출처 정책이라고도 함). window.postMessage() 메서드는 서로 다른 출처 간에 안전하게 통신하기 위한 제어된 메커니즘을 제공합니다. 이는 채팅 애플리케이션, 공동작업 도구 등을 구현하는 데 유용할 수 있습니다. 예를 들어 채팅 애플리케이션은 postMessage를 사용하여 서로 다른 웹사이트에 있는 사용자 간에 메시지를 보낼 수 있습니다. 신뢰할 수 있는 웹 활동 (TWA)에서 postMessage를 사용하는 것은 약간 까다로울 수 있습니다. 이 가이드에서는 TWA 클라이언트에서 postMessage를 사용하여 웹페이지로 메시지를 보내고 웹페이지에서 메시지를 받는 방법을 설명합니다.

웹 유효성 검사에 앱 추가

postMessage API를 사용하면 두 개의 유효한 출처(소스 및 대상 출처)가 서로 통신할 수 있습니다. Android 애플리케이션이 대상 출처로 메시지를 전송하려면 어떤 소스 출처와 동일한지 선언해야 합니다. 디지털 애셋 링크 (DAL)를 사용하여 assetlinks.json 파일에 앱의 패키지 이름을 관계가 use_as_origin인 상태로 추가하면 다음과 같이 됩니다.

[{
  "relation": ["delegate_permission/common.use_as_origin"],
  "target" : { "namespace": "android_app", "package_name": "com.example.app", "sha256_cert_fingerprints": [""] }
}]

TWA와 연결된 출처에서 설정할 때는 MessageEvent.origin 필드의 출처를 제공해야 하지만 postMessage는 디지털 애셋 링크가 포함되지 않은 다른 사이트와 통신하는 데 사용할 수 있습니다. 예를 들어 www.example.com를 소유한 경우 DAL을 통해 이를 증명해야 하지만 다른 웹사이트(예: www.wikipedia.org)와 통신할 수는 있습니다.

매니페스트에 PostMessageService 추가

postMessage 통신을 수신하려면 서비스를 설정해야 합니다. Android 매니페스트에 PostMessageService를 추가하여 설정합니다.

<service android:name="androidx.browser.customtabs.PostMessageService"
android:exported="true"/>

CustomTabsSession 인스턴스 가져오기

매니페스트에 서비스를 추가한 후 CustomTabsClient 클래스를 사용하여 서비스를 바인딩합니다. 연결되면 제공된 클라이언트를 사용하여 다음과 같이 새 세션을 만들 수 있습니다. CustomTabsSession은 postMessage API를 처리하는 핵심 클래스입니다. 다음 코드는 서비스가 연결되면 클라이언트가 새 세션을 만드는 데 사용되고 이 세션이 postMessage에 사용되는 방법을 보여줍니다.

private CustomTabsClient mClient;
private CustomTabsSession mSession;

// We use this helper method to return the preferred package to use for
// Custom Tabs.
String packageName = CustomTabsClient.getPackageName(this, null);

// Binding the service to (packageName).
CustomTabsClient.bindCustomTabsService(this, packageName, new CustomTabsServiceConnection() {
 @Override
 public void onCustomTabsServiceConnected(@NonNull ComponentName name,
     @NonNull CustomTabsClient client) {
   mClient = client;

   // Note: validateRelationship requires warmup to have been called.
   client.warmup(0L);

   mSession = mClient.newSession(customTabsCallback);
 }

 @Override
 public void onServiceDisconnected(ComponentName componentName) {
   mClient = null;
 }
});

이제 이 customTabsCallback 인스턴스가 무엇인지 궁금하실 겁니다. 다음 섹션에서 이를 만들겠습니다.

CustomTabsCallback 만들기

CustomTabsCallback은 CustomTabsClient가 맞춤 탭의 이벤트와 관련된 메시지를 수신하기 위한 콜백 클래스입니다. 이러한 이벤트 중 하나는 onPostMessage이며 앱이 웹에서 메시지를 수신할 때 호출됩니다. 다음 코드와 같이 클라이언트에 콜백을 추가하여 postMessage 채널을 초기화하여 통신을 시작합니다.

private final String TAG = "TWA/CCT-PostMessageDemo";

// The origin the TWA is equivalent to, where the Digital Asset Links file
// was created with the "use_as_origin" relationship.
private Uri SOURCE_ORIGIN = Uri.parse("https://source-origin.example.com");

// The origin the TWA will communicate with. In most cases, SOURCE_ORIGIN and
// TARGET_ORIGIN will be the same.
private Uri TARGET_ORIGIN = Uri.parse("https://target-origin.example.com");

// It stores the validation result so you can check on it before requesting
// postMessage channel, since without successful validation it is not possible
// to use postMessage.
boolean mValidated;

CustomTabsCallback customTabsCallback = new CustomTabsCallback() {

    // Listens for the validation result, you can use this for any kind of
    // logging purposes.
    @Override
    public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin,
        boolean result, @Nullable Bundle extras) {
        // If this fails:
        // - Have you called warmup?
        // - Have you set up Digital Asset Links correctly?
        // - Double check what browser you're using.
        Log.d(TAG, "Relationship result: " + result);
        mValidated = result;
    }

    // Listens for any navigation happens, it waits until the navigation finishes
    // then requests post message channel using
    // CustomTabsSession#requestPostMessageChannel(sourceUri, targetUri, extrasBundle)

    // The targetOrigin in requestPostMessageChannel means that you can be certain their messages are delivered only to the website you expect.
    @Override
    public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
        if (navigationEvent != NAVIGATION_FINISHED) {
            return;
        }

        if (!mValidated) {
            Log.d(TAG, "Not starting PostMessage as validation didn't succeed.");
        }

        // If this fails:
        // - Have you included PostMessageService in your AndroidManifest.xml ?
        boolean result = mSession.requestPostMessageChannel(SOURCE_ORIGIN, TARGET_ORIGIN, new Bundle());
        Log.d(TAG, "Requested Post Message Channel: " + result);
    }

    // This gets called when the channel we requested is ready for sending/receiving messages.
    @Override
    public void onMessageChannelReady(@Nullable Bundle extras) {
        Log.d(TAG, "Message channel ready.");

        int result = mSession.postMessage("First message", null);
        Log.d(TAG, "postMessage returned: " + result);
    }

    // Listens for upcoming messages from Web.
    @Override
    public void onPostMessage(@NonNull String message, @Nullable Bundle extras) {
        super.onPostMessage(message, extras);
        // Handle the received message.
    }
};

웹에서 통신

이제 호스트 앱에서 메시지를 주고받을 수 있습니다. 웹에서도 동일하게 하려면 어떻게 해야 하나요? 통신은 호스트 앱에서 시작해야 하며, 그런 다음 웹페이지는 첫 번째 메시지에서 포트를 가져와야 합니다. 이 포트는 다시 통신하는 데 사용됩니다. JavaScript 파일은 다음 예와 같이 표시됩니다.

window.addEventListener("message", function (event) {
  // We are receiveing messages from any origin, you can check of the origin by
  // using event.origin

  // get the port then use it for communication.
  var port = event.ports[0];
  if (typeof port === 'undefined') return;

  // Post message on this port.
  port.postMessage("Test")

  // Receive upcoming messages on this port.
  port.onmessage = function(event) {
    console.log("[PostMessage1] Got message" + event.data);
  };
});

전체 샘플은 여기에서 확인할 수 있습니다.

사진: Unsplash조안나 코신스카