TWA를 위한 PostMessage

Sayed El-Abady
Sayed El-Abady

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

이 가이드를 마치면 다음을 할 수 있습니다.

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

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

  • 최신 androidx.browser (최소 v1.6.0-alpha02) 라이브러리를 build.gradle 파일에 추가합니다.
  • 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 제공)