[Android] Trusted Web Activity Guide

김병수·2024년 7월 3일
0
post-thumbnail

이번 포스팅에서는 Chrome 개발자 문서에서 언급하고 있는 Trusted Web Activity(이하 TWA) Guide를 정리해 보고자 합니다.
실제 문서에는 총 8개의 섹션이 존재하지만, 본 포스팅에서는 5개의 섹션만을 다룰 예정입니다👋🏻

  • Multi-origin Trusted Web Activity
  • Query Parameters
  • Android browser helper library
  • Web Share Target
  • Offline-first Trusted Web Activity
  • PostMessage
    (PostMessage 섹션은 내용이 많은 관계로, 따로 하나의 포스팅으로 작성할 예정입니다.)

Multi-origin Trusted Web Activity

TWA에서 여러 개의 호스트로 이루어진 웹 사이트를 Full Screen으로 제공하기 위한 방법에 대하여

Multi-origin?

Multi-origin이란 말 그대로 origin이 여러 개인 경우를 의미합니다.

여기서 Origin에 대해 간단한 부연 설명을 덧붙이자면,
2개의 URL이 서로 같은 protocol, port number, host를 갖는 경우same-origin 이라고 표현합니다.
반대로 protocol, port number, host 중 하나라도 다른 경우는 origin이 다른 경우가 되며, 다시 말하면 multi-origin이 되는 것이죠.

Multi-origin의 필요성

기본적으로 TWA는 Digital Asset Links를 사용하여 앱 소유자와 동일한 웹만을 Full Screen으로 제공합니다. 그리고 TWA에서 인증되지 않은 웹 사이트로 이동하는 경우, Custom Tab UI로 접속된다는 사실을 지난 포스팅을 통해 알게 되었는데요.

이러한 방식은 사용자에게 보안 측면에서 안전장치를 제공한다는 측면에서는 장점이지만, 사용자 경험을 헤친다는 점에서는 단점으로 작용하게 됩니다.

하지만 웹 앱에서 여러 host로 이루어진 웹 사이트를 사용하는 것은 매우 흔한 케이스입니다.
실제로 대부분의 쇼핑 웹 앱의 경우, 메인 기능은 www.example.com에서 제공하고 결제 기능은 따로 checkout.example.com와 같이 별도의 도메인을 사용하고 있습니다.

이러한 웹 앱을 TWA로 제공할 때 multi-origin을 지원하지 않는다면, 사용자는 결제 과정에서 앱을 벗어났다는 느낌을 받을 수 있습니다. 그렇기 때문에 TWA는 반드시 multi-origin을 제공해야 합니다.

TWA에서 Multi-origin을 사용하는 방법

assetlinks.json 파일 호스팅하기

우선 TWA에서 지원할 모든 도메인에 동일한 assetlinks.json 파일을 업로드해야 합니다.
위에서 언급한 예시의 경우에는 아래와 같은 위치에 파일을 호스팅해야 합니다.

  • https://www.example.com/.well-known/assetlinks.json
  • https://checkout.example.com/.well-known/assetlinks.json

안드로이드 앱에 multi-origin 추가하기

res/values/string.xml 파일에 아래와 같은 string 리소스를 추가해서 사용하거나

<string name="asset_statements">
[{
	\"relation\": [\"delegate_permission/common.handle_all_urls\"],
	\"target\": {
		\"namespace\": \"web\",
		\"site\": \"https://www.example.com\"
	}
}],
[{
	\"relation\": [\"delegate_permission/common.handle_all_urls\"],
	\"target\": {
		\"namespace\": \"web\",
		\"site\": \"https://checkout.example.com\"
	}
}],
...
</string>

app/build.gradle 파일에 문자열을 직접 추가해서 사용할 수 있습니다.

resValue "string", "assetStatements", """
    [{
		\"relation\": [\"delegate_permission/common.handle_all_urls\"],
		\"target\": {
			\"namespace\": \"web\",
			\"site\": \"https://www.example.com\"
		}
	}],
	[{
		\"relation\": [\"delegate_permission/common.handle_all_urls\"],
		\"target\": {
			\"namespace\": \"web\",
			\"site\": \"https://checkout.example.com\"
		}
	}],
..."""

Default LauncherActivity를 사용하는 경우

res/values/strnig.xml 파일에 사용할 multi-origin <item>으로 구성된 <string-array>를 추가합니다.

<string-array name="additional_trusted_origins">
	<item>https://www.google.com</item>
</string-array>

그리고 이를 AndroidManifest.xml 파일의 LauncherActivity 내부에 <meta-data>로 추가합니다.

<activity
	android:name="com.google.androidbrowserhelper.trusted.LauncherActivity"
	android:label="@string/app_name">
		<meta-data
			android:name="android.support.customtabs.trusted.ADDITIONAL_TRUSTED_ORIGINS"
			android:resource="@array/additional_trusted_origins" />
</activity>

Custom LauncherActivity를 사용하는 경우

Custom LauncherActivity를 사용하는 경우에는 TrustedWebActivityIntentBuilder.setAdditionalTrustedOrigins을 사용하여 extra origins을 추가할 수 있습니다.

public void launcherWithMultipleOrigins(View view) {
	List<String> origins = Arrays.asList("https://checkout.example.com/");
	TrustedWebActivityIntentBuilder builder = new TrustedWebActivityIntentBuilder(LAUNCH_URI)
		.setAdditionalTrustedOrigins(origins);
		
	new TwaLauncher(this).launch(builder, null, null);
}

Multi-origin 설정과 관련된 샘플 프로젝트는 Github에서 확인하실 수 있습니다.


Query Parameters

Query Parameter를 사용하여 네이티브에서 PWA로 데이터를 전달하는 방법에 대하여

이 섹션에서는 TWA로 연결할 웹 사이트의 URL에 Query Parameter를 추가하는 방법에 대해 소개합니다.

URL에 Query Parameter를 추가할 필요가 있나요?

제가 이 섹션을 읽을 때 들었던 생각인데요.
주로 TWA에서 시작되는 세션과 설치 횟수 등을 측정하기 위한 Google Custom Analytics Segmentation을 구현할 때 사용한다고 합니다.

다시 생각해 보니, 이 외에도 상황에 따라 Query Parameter가 필요할 수 있을 것 같다는 생각이 드네요🤔

Default LauncherActivity를 사용하는 경우 (Start URL 정적 변경)

AndroidManifest.xml 파일의 LauncherActivity 내부에 위치한 DEFAULT_URL <meta-data>을 변경해 주면 됩니다.

<activity android:name="com.google.androidbrowserhelper.trusted.LauncherActivity"
		  android:label="@string/app_name">
	...
	<meta-data android:name="android.support.customtabs.trusted.DEFAULT_URL"
			android:value="https://svgomg.firebaseapp.com/?utm_source=trusted-web-activity" />
	...
</activity>

Custom LauncherActivity를 사용하는 경우 (Start URL 동적 변경)

우선 LauncherActivity를 상속받는 Custom LauncherActivity 클래스에서 getLaunchingUrl() 함수를 오버라이딩 해야 합니다.

public class CustomQueryStringLauncherActivity extends LauncherActivity {

	private String getDynamicParameterValue() {
		return String.valueOf((int)(Math.random() * 1000));
	}

	@Override
	protected Uri getLaunchingUrl() {
      // Get the original launch Url.
      Uri uri = super.getLaunchingUrl();

      // Get the value we want to use for the parameter value
      String customParameterValue = getDynamicParameterValue();

      // Append the extra parameter to the launch Url
      return uri
              .buildUpon()
              .appendQueryParameter("my_parameter", customParameterValue)
              .build();
	}
    
}

그리고 Custom LauncherActivity 사용을 위해, AndroidManifest.xml 파일을 수정해야 합니다.

<activity android:name="com.myapp.CustomQueryStringLauncherActivity"
		  android:label="@string/app_name">
	...
	<meta-data android:name="android.support.customtabs.trusted.DEFAULT_URL"
			android:value="https://squoosh.app/?utm_source=trusted-web-activity" />
	...
</activity>

주의 사항

이러한 방식으로 추가된 Query Parameter는 Script가 실행되는 동안 사용될 수 있으며, 사용자가 다른 페이지로 이동하거나 웹 개발자가 공유 기능을 구현할 때 사용될 수 있습니다.

그렇기 때문에 앱 개발자는 민감한 데이터를 Query Parameter로 전달하는 것을 지양해야 하며, link rel=noreferrer를 사용하거나 page location API을 사용하여 URL을 깔끔하게 정리하는 방법도 고려할 수 있다고 합니다.


Android browser helper library

2020년 1월 8일에 1.0.0 버전(stable)이 배포된 android-browser-helper 라이브러리를 사용하세요

android-borwser-helper?

android-browser-helper는 TWA를 사용하기 위해 만들어진 공식 라이브러리 입니다.

AndroidX와의 호환성 문제 및 기존에 사용하던 custom-tabs-client 라이브러리의 여러 문제점을 해결했으며, 아래와 같은 새로운 기능도 추가되었다고 합니다.

  • TWA를 지원하는 브라우저에서 컨텐츠를 여는 방법을 제어할 수 있음
  • TWA를 지원하는 브라우저가 설치되어 있지 않은 경우, fallback 전략을 개발자가 커스텀할 수 있음
  • twa-webview-fallback에서는 Android WebView를 사용하여 웹 컨텐츠를 보여주는 방식으로 fallback 전략을 커스텀한 예를 보여줌

android-browser-helper를 사용하기 위한 의존성은 아래와 같습니다.

// app/build.gradle
dependencies {
	//...
	implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.2.0'
}

custom-tabs-client에서 android-browser-helper로 마이그레이션 하기

다행히도 custom-tabs-client에서 android-browser-helper로 마이그레이션 하는 과정은 복잡하지 않고, 몇 가지 문자열만 수정하면 된다고 합니다.
정확히 어떤 문자열을 수정해야 하는지는 svgomg-twa 레포지토리에서 확인하실 수 있습니다.

추가로 custom-tabs-client에서 제공하는 기능에 대응되는 android-browser-helper의 기능은 아래와 같습니다.

Name on custom-tabs-client (Old Library)Name on android-browser-helper (New Library)
android.support.customtabs.trusted.LauncherActivitycom.google.androidbrowserhelper.trusted.LauncherActivity
android.support.v4.content.FileProviderandroidx.core.content.FileProvider
android.support.customtabs.trusted.TrustedWebActivityServicecom.google.androidbrowserhelper.trusted.DelegationService

Web Share Target

PWA에서 가능한 Web Share Target 기능을 TWA에서 사용하는 방법에 대하여

Web Share Target 이란?

Web Share Target은 기기에 설치되어 있는 다른 앱으로부터 데이터를 공유받기 위해 사용하는 API입니다.

예를 들어 이미지 편집 앱의 경우에는 카메라 앱으로부터 사진을 받아오는 과정에 사용할 수 있으며, SNS 앱의 경우에는 공유를 목적으로 하는 이미지 또는 영상 파일을 받아오는 과정에 사용할 수 있습니다.

TWA에서 Web Share Target 사용하기 위한 준비

android-browser-helper 라이브러리 버전 설정

Web Share Target을 사용하려면 android-browser-helper 라이브러리의 버전을 2.0.1 이상으로 설정해야 한다고 합니다.

// app/build.gradle
dependencies {
	...
	implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.0.1'}

res/string.xml 파일 수정

개발자는 TWA에게 어떤 데이터를 어떻게 받아올 것인지 미리 알려주어야 합니다.
(가령 데이터를 받아서 어떤 URL로 연결할 것인지, 사용할 Method(GET, POST, etc)는 무엇인지, 지원하는 mime-type은 무엇인지 등)

이러한 정보는 JSON 형태의 문자열로 제공해야 하며, Web Manifest 내부의 share_target 필드와 거의 동일합니다.

// Web Manifest
{
	...
	"share_target": {
		"action": "/_share-target",
		"enctype": "multipart/form-data",
		"method": "POST",
		"params": {
			"files": [{
				"name": "media",
				"accept": [
					"audio/*",
					"image/*",
					"video/*"
				]
			}]
		}
	},
	...
}

위와 같은 share_target에서 아래의 2가지 변경 사항을 적용하면 됩니다.

  • action 속성을 Full URL 형태로 작성해야 합니다. (origin이 포함된 URL만 가능)
    ex) https://twa-web-scrapbook.web.app/_share-target
  • 큰따옴표를 사용할 때, \"로 사용해야 함

이러한 변경 사항을 적용한 share_target을 아래와 같이 res/values/string.xml파일에 문자열 리소스로 추가합니다.

<string name="share_target">
{
	\"action\": \"https://twa-web-scrapbook.web.app/_share-target\",
	\"method\": \"POST\",
	\"enctype\": \"multipart/form-data\",
	\"params\": {
		\"files\": [{
			\"name\": \"media\",
			\"accept\": [\"image/*\", \"audio/*\", \"video/*\"]
		}]
	}
}
</string>

AndroidManifest.xml 파일 수정

Web Share Target을 사용하기 위해 DelegationService를 추가합니다.

<service android:name="com.google.androidbrowserhelper.trusted.DelegationService"
	android:enabled="true"
	android:exported="true">
	<intent-filter>
		<action android:name="android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE"/>
		<category android:name="android.intent.category.DEFAULT"/>
	</intent-filter>
</service>

그리고 <meta-data> 태그와 <intent-filter> 태그를 추가합니다.
여기서 <intent-filter>에는 share_target에서 정의한 모든 mimeType을 추가해야 합니다.

<meta-data
	android:name="android.support.customtabs.trusted.METADATA_SHARE_TARGET"
	android:resource="@string/share_target"/>

<intent-filter>
	<action android:name="android.intent.action.SEND" />
	<action android:name="android.intent.action.SEND_MULTIPLE" />
	<category android:name="android.intent.category.DEFAULT" />
	<data android:mimeType="audio/*" />
	<data android:mimeType="image/*" />
	<data android:mimeType="video/*" />
</intent-filter>

이렇게 하면 Web Share Target 기능을 TWA에서도 사용할 수 있다고 합니다.

Troubleshooting

Q1. My application doesn't show as an option when I try to share a file from another application. (다른 앱에서 공유 버튼을 눌렀는데, 내 TWA 앱이 표시되지 않아요.)

이러한 문제는 대부분 <intent-filter>가 잘못 선언되어 있기 때문에 발생하는 문제로써, <intent-filter> 내부의 action, category, mimeType 등이 정확하게 작성되어 있는지 확인하는 것을 추천한다고 합니다.

Q2. My application shows as an option, the PWA is started, but the data is not shared. (공유 옵션으로 표시 되고, PWA가 실행되지만 데이터는 공유되지 않아요.)

이러한 경우에는 Digital Asset Links가 유효한지 확인하거나, res/string.xml 파일 내부의 JSON 파일이 정확한지 확인하는 것을 추천한다고 합니다.


Offline-first Trusted Web Activity

TWA를 사용하면서 First Time 및 Offline 대처하기

TWA를 통해 PWA를 제공하는 경우, 아래와 같은 2가지 경우를 마주할 수 있습니다.

  • Registration Process가 아직 완료되지 않아서 Service Worker가 등록되지 않은 경우 (TWA로 처음 접속하는 경우)
  • 네트워크 연결이 없는 상태에서 TWA로 접속하는 경우

이러한 경우를 대비하여 Custom Fallback 동작을 구현한다면, 사용자에게 더 좋은 경험을 제공할 수 있습니다.
그렇다면 Custom Fallback 동작은 어떻게 구현하는 걸까요?

Custom Fallback 동작 구현 방법

Custom LauncherActivity 생성

Custom Fallback 동작을 구현하기 위해서는 Custom LauncherActivity를 사용해야 합니다.
아래와 같이 LauncherActivity를 상속받는 클래스를 만들고, 이를 AndroidManifest.xml 파일에 추가합니다.

import com.google.androidbrowserhelper.trusted.LauncherActivity;
	
public class OfflineFirstTWALauncherActivity extends LauncherActivity {

}
<activity android:name=".OfflineFirstTWALauncherActivity" android:theme="@style/Theme.Design.NoActionBar">
	<intent-filter>
		<action android:name="android.intent.action.MAIN" />
		<category android:name="android.intent.category.LAUNCHER" />
	</intent-filter>
	<!-- Edit android:value to change the url opened by the Trusted Web Activity -->
	<meta-data 
		android:name="android.support.customtabs.trusted.DEFAULT_URL"
        android:value="https://airhorner.com" />
		<!-- This intent-filter adds the Trusted Web Activity to the Android Launcher -->
	<intent-filter>
		<action android:name="android.intent.action.VIEW" />
		<category android:name="android.intent.category.DEFAULT" />
		<category android:name="android.intent.category.BROWSABLE" />
		<!-- Edit android:host to handle links to the target URL -->
		<data android:host="airhorner.com" android:scheme="https" />
	</intent-filter>
</activity>

여기서 DEFAULT_URL 값과 <intent-filter> 내부의 host 및 scheme를 변경해서 사용해야 합니다.

Custom LauncherActivity 기능 구현

Custom LauncherActivity에서 shouldLaunchImmediately() 함수를 오버라이딩 합니다.
shouldLaunchImmediately() 함수가 false를 리턴하면, TWA의 실행을 launchTwa() 함수 호출을 통해 제어할 수 있게 됩니다.

@Override
protected boolean shouldLaunchImmediately() {
	// launchImmediately() returns `false` so we can check connection
	// and then render a fallback page or launch the Trusted Web Activity with `launchTwa()`.
	return false;
}

그리고 onCreate() 함수에서 아래와 같이 네트워크 연결 상태 및 TWA를 이전에 실행한 적 있는지 확인하여 분기 처리하면 된다고 합니다.

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	tryLaunchTwa();
}

private void tryLaunchTwa() {
	// If TWA has already launched successfully, launch TWA immediately.
	// Otherwise, check connection status. If online, launch the Trusted Web Activity with `launchTwa()`.
	// Otherwise, if offline, render the offline fallback screen.
	if (hasTwaLaunchedSuccessfully()) {
		launchTwa();
	} else if (isOnline()) {
		firstTimeLaunchTwa();
	} else {
		renderOfflineFallback();
	}
}

private boolean isOnline() {
	ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
	NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
	return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}

private boolean hasTwaLaunchedSuccessfully() {
	// Return `true` if the preference "twa_launched_successfully" has already been set.
	SharedPreferences sharedPref = getSharedPreferences(getString(R.string.twa_offline_first_preferences_file_key), Context.MODE_PRIVATE);
	return sharedPref.getBoolean(getString(R.string.twa_launched_successfully), false);
}

private void renderOfflineFallback() {
	setContentView(R.layout.activity_offline_first_twa);
	
	Button retryBtn = this.findViewById(R.id.retry_btn);
	retryBtn.setOnClickListener(new View.OnClickListener() {
		public void onClick(View v) {
			// Check connection status. If online, launch the Trusted Web Activity for the first time.
			if (isOnline())
				firstTimeLaunchTwa();
		}
	});
}

이상으로 Chrome 개발자 문서에 작성되어 있는 TWA Guide 내용을 정리해 보았습니다.
TWA 관련 내용을 정리하면서 웹 관련 지식도 하나씩 늘어가고 있는 것 같은데요.
잘못된 내용이 있다면 지적 부탁드립니다 🙇🏻‍♂️

profile
주니어 개발자

0개의 댓글