일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- ar vr 차이
- 안드로이드 레트로핏 crud
- 객체
- 안드로이드 os 구조
- 멤버변수
- 안드로이드 유닛 테스트 예시
- rxjava cold observable
- 안드로이드 레트로핏 사용법
- android retrofit login
- ANR이란
- 안드로이드 라이선스
- 스택 큐 차이
- android ar 개발
- 플러터 설치 2022
- 서비스 쓰레드 차이
- 큐 자바 코드
- 안드로이드 유닛테스트란
- 스택 자바 코드
- 안드로이드 유닛 테스트
- jvm 작동 원리
- rxjava disposable
- jvm이란
- 2022 플러터 안드로이드 스튜디오
- 클래스
- 서비스 vs 쓰레드
- 자바 다형성
- rxjava hot observable
- Rxjava Observable
- 안드로이드 라이선스 종류
- 2022 플러터 설치
- Today
- Total
나만을 위한 블로그
[Android] 앱스플라이어 원링크 사용법 본문
※ 모든 코드는 예시 코드기 때문에 실제로 사용하려면 반드시 리팩토링해야 한다
※ 디퍼드 딥링크는 확인하지 않는다
지난달 8월 25일 부로 파이어베이스 다이나믹 링크가 지원 종료되었다.
https://firebase.google.com/support/dynamic-links-faq?hl=ko
동적 링크 지원 중단 FAQ | Firebase
의견 보내기 동적 링크 지원 중단 FAQ 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 참고: Firebase 동적 링크는 지원 중단되었으므로 새 프로젝트에서 사용해
firebase.google.com
대체제로 안드로이드는 앱 링크를 사용할 수 있지만 파이어베이스 다이나믹 링크에 비해 지원되지 않는 부분이 많아 보인다.
아래는 파이어베이스 공식문서 중 다이나믹 링크, 앱 링크 별 지원 기능을 비교한 표다.
앱 링크는 디퍼드 딥링크의 역할을 수행할 수 없고, 그 외 이런저런 기능들도 다이나믹 링크에 비해 제한되는 부분이 많다.
간단한 딥링크 정도라면 앱 링크를 써서 구현할 수 있겠지만 그 외의 기능도 포함한 딥링크를 원한다면 서드파티 라이브러리를 사용하는 것이 강제로 고려되는 상황이다.
대체제로 에어브릿지, AB180, 앱스플라이어가 있는데 이 포스팅에선 앱스플라이어가 제공하는 원링크를 어떻게 사용하는지 확인해 본다.
대시보드 생성은 앱스플라이어 공식문서와 다른 미디엄 포스팅을 보면 할 수 있으니 생략한다.
아래부터 쓰인 내용은 몇 달 전에 테스트한 내용이라 중간중간 누락된 부분이 있다.
아래 문서를 보면서 초기 설정을 진행하고 다른 부분이나 코드가 필요하다면 이 포스팅을 보면 될 것이다.
https://support.appsflyer.com/hc/ko/sections/6551015977105
대시보드 생성이 완료되면 아래 화면이 표시될 것이다.
여기 표시되는 dev key는 어딘가에 잘 보관해둔다.
이벤트 설정은 만들려는 딥링크의 성격에 맞게 알아서 설정한다. 애매하다면 기획자한테 물어보는 것도 좋다.
다 만들고 나면 템플릿을 설정하라는 작은 창이 뜬다.
iOS는 무시하고 안드로이드 밑 토글에서 내 앱을 선택한 뒤 서브도메인을 설정해 준다. 주의사항과 문서를 잘 확인해서 일 2번 하지 않게 서브도메인을 잘 설정하자.
다음 버튼을 누르면 아래 화면이 나올 것이다.
이것도 적당히 설정하고 템플릿 만들기를 누르면 아래 화면이 표시된다.
이제 준비가 끝났으니 원링크를 만든다. 원링크를 만드는 순서는 아래 문서에 잘 나와 있다.
왼쪽 탭에서 인게이지 탭을 클릭하면 밑에 딥링크 관리라는 탭이 보인다. 이걸 클릭한다.
그럼 아래와 같은 화면이 나올 것이다.
새 링크를 누르면 만들 수 있는 원링크 종류들이 표시된다.
여기서도 만들려는 딥링크의 성격에 맞춰 잘 선택해 진행한다. 여기선 텍스트 투 앱을 선택해 진행한다.
텍스트 투 앱 뿐 아니라 다른 항목을 선택하면 추가로 라디오 버튼이나 입력란이 표시된다.
텍스트 투 앱은 SMS 관련 원링크를 만드는 만큼 관련 옵션들이 표시된다. SMS를 선택하고 다음을 클릭하면 아래 화면이 보인다.
제목인 Text-to-app 문자열은 원하는 이름으로 바꿀 수 있다. 여기선 그냥 진행한다.
캠페인 이름, 리타겟팅 등 여러 옵션이 있는데 이 부분부터는 기획자 옆에 앉혀놓거나 화면 공유로 보여주면서 같이 진행하는 게 좋을 것이다. 여기선 예제기 때문에 내 맘대로 설정한다.
다음을 눌러 다음 설정을 진행한다.
소셜 앱 랜딩 페이지 만들기 버튼을 누르면 아래 화면이 뜬다.
선택하면 이런저런 요소를 커스텀할 수 있는 화면으로 이동한다.
여기선 무시한다. 다시 다음을 누르면 아래 화면이 보인다.
이제부턴 딥링크에 어떤 값을 담을지와 리디렉션 시 유저를 어디로 이동시킬지 정할 수 있다.
플레이 스토어에 올라가 있는 앱이라면 리디렉션 위치를 앱 설치 화면으로 설정할 수 있고 추가 설정을 통해 디퍼드 딥링크를 만들 수 있을 것이다.
난 스토어에 올라가지 않은 상태에서 원링크를 테스트하기 때문에 딥링크 값, 추가 딥링크 값만 설정했다.
딥링크 값은 deep_link_value, 추가 딥링크 값은 deep_link_sub1로 링크에 표시된다. 각 텍스트 오른쪽의 ?에 마우스를 올리면 확인할 수 있다.
설정됐다면 다음을 누른다.
파라미터 추가를 누르면 원링크가 기본 제공하는 파라미터들 중에서 원하는 쿼리 파라미터를 추가할 수 있다.
af_sub1을 선택하면 아래처럼 값을 설정할 수 있다.
규모 있는 회사라면 여기까지 기획자가 알아서 설정하고 링크를 공유해 주겠지만 그렇지 않을 수도 있다. 애초에 이런 설정법을 알아둬서 나쁠 건 없으니 이것저것 클릭해 보면서 익혀두면 좋을 것이다.
모두 끝났다면 오른쪽에 표시되는 링크 미리보기를 보고 내가 잘 만든 게 맞는지 한 번 확인해 준다.
문제 없다면 링크 생성을 누른다. 그럼 아래 화면이 표시될 것이다.
단축 URL, 긴 URL을 각각 비교하면 아래와 같다.
// 단축 URL
https://regacyviewl.onelink.me/zNHT/cdu65o1d
// 긴 URL
https://regacyviewl.onelink.me/zNHT?af_xp=text&pid=SMS&c=%EC%BA%A0%ED%8E%98%EC%9D%B81&deep_link_value=id&deep_link_sub1=1&af_sub1=test
긴 URL을 누르면 내가 앞서 설정한 값들이 원링크에 포함돼 있는 걸 볼 수 있다.
어느 걸 써도 똑같으니 맘에 드는 걸 복사해서 메모장 같은 곳에 복붙해둔 다음 안드로이드 설정을 이어서 진행한다.
앱 gradle에 의존성을 추가한다. 오늘 기준 최신 버전은 6.17.3이다.
implementation("com.appsflyer:af-android-sdk:6.16.2")
그리고 Application을 상속하는 클래스를 작성한다. 이것은 당연히 매니페스트에 등록돼야 한다.
Timber는 로그 라이브러리인데 안 쓸 경우 없애면 된다.
import android.app.Application
import com.appsflyer.AppsFlyerLib
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
@HiltAndroidApp
class App: Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
AppsFlyerLib.getInstance().apply {
init("iMjYPqE2jdQKJiYLdT3phi", null, this@App)
start(this@App)
setDebugLog(true) // 테스트용 앱스플라이어 디버그 모드 활성화
}
}
}
그리고 매니페스트를 수정한다. 딥링크 처리를 추가해 주고 앱스플라이어의 설정이 앱 설정을 뒤집어쓰지 않게 tools:replace 부분을 추가했다.
이 부분은 앱스플라이어의 안드 개발자를 위한 문서가 따로 존재하니 이 부분을 같이 확인하면서 진행한다. iOS 문서도 있으니 필요하다면 공유해 주자.
https://dev.appsflyer.com/hc/docs/android-sdk
Android SDK
AppsFlyer Android SDK guides for developers.
dev.appsflyer.com
<application
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RegacyViewExample"
tools:targetApi="31"
tools:replace="android:dataExtractionRules,android:fullBackupContent">
<!-- 앱의 backup, data extraction 규칙이 앱스플라이어 SDK의 설정을 뒤집어쓰게 함 -->
<activity
android:name=".presentation.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- 원링크 도메인 -->
<data
android:scheme="https"
android:host="regacyviewl.onelink.me" />
</intent-filter>
</activity>
</application>
원링크 도메인은 내 코드를 기준으로 썼기 때문에 반드시 본인의 도메인으로 바꿔줘야 한다.
아래는 메인 액티비티의 코드다.
import android.content.Intent
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import com.appsflyer.AppsFlyerLib
import com.appsflyer.deeplink.DeepLinkListener
import com.appsflyer.deeplink.DeepLinkResult
import com.example.regacyviewexample.R
import com.example.regacyviewexample.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = true
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
setupDeepLinkListener()
// 처음 앱 실행 시 딥링크 처리
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
val data = intent.data
Timber.d("## [onNewIntent] data : $data")
handleDeepLink(intent)
}
private fun setupDeepLinkListener() {
AppsFlyerLib.getInstance().subscribeForDeepLink(object : DeepLinkListener {
override fun onDeepLinking(deepLinkResult: DeepLinkResult) {
Timber.d("## [딥링크] deepLinkResult : $deepLinkResult")
when (deepLinkResult.status) {
DeepLinkResult.Status.FOUND -> {
val deepLinkObj = deepLinkResult.deepLink
val deepLinkValue = deepLinkObj.deepLinkValue
val campaign = deepLinkObj.campaign
val mediaSource = deepLinkObj.mediaSource
lifecycleScope.launch {
binding.run {
tvTest.text = "deepLinkObj : $deepLinkObj, deepLinkValue : $deepLinkValue, campaign : $campaign, mediaSource : $mediaSource"
}
}
Timber.d("## [딥링크] deepLinkObj : $deepLinkObj, deepLinkValue : $deepLinkValue, campaign : $campaign, mediaSource : $mediaSource")
}
DeepLinkResult.Status.NOT_FOUND -> {
Timber.d("## [딥링크] 딥링크 없음")
}
DeepLinkResult.Status.ERROR -> {
Timber.e("## [딥링크] 에러 발생 : ${deepLinkResult.error}")
}
}
}
})
}
private fun handleDeepLink(intent: Intent) {
Timber.d("## [handleDeepLink] intent : $intent, data: ${intent.data}")
AppsFlyerLib.getInstance().performOnDeepLinking(intent, this)
}
}
subscribeForDeepLink()는 아래처럼 람다로 바꿔 쓸 수 있다고 IDE가 권장하지만 이 부분은 개취기 때문에 맘에 드는 방식으로 작성한다.
private fun setupDeepLinkListener() {
AppsFlyerLib.getInstance().subscribeForDeepLink { deepLinkResult ->
Timber.d("## [딥링크] deepLinkResult : $deepLinkResult")
when (deepLinkResult.status) {
DeepLinkResult.Status.FOUND -> {
val deepLinkObj = deepLinkResult.deepLink
val deepLinkValue = deepLinkObj.deepLinkValue
val campaign = deepLinkObj.campaign
val mediaSource = deepLinkObj.mediaSource
lifecycleScope.launch {
binding.run {
tvTest.text = "deepLinkObj : $deepLinkObj, deepLinkValue : $deepLinkValue, campaign : $campaign, mediaSource : $mediaSource"
}
}
Timber.d("## [딥링크] deepLinkObj : $deepLinkObj, deepLinkValue : $deepLinkValue, campaign : $campaign, mediaSource : $mediaSource")
}
DeepLinkResult.Status.NOT_FOUND -> {
Timber.d("## [딥링크] 딥링크 없음")
}
DeepLinkResult.Status.ERROR -> {
Timber.e("## [딥링크] 에러 발생 : ${deepLinkResult.error}")
}
}
}
}
이제 앱을 실행하면 된다. 이 때 앞서 복붙해뒀던 원링크를 준비한다. 만약 잊어버렸다면 원링크 관리 화면에서 다시 가져올 수 있다.
딥링크 테스트는 너무 귀찮기 때문에 adb 명령어를 쓰면 편하게 테스트할 수 있다.
안드로이드 스튜디오 터미널에서 아래 명령어를 실행하면 된다.
adb shell am start -W -a android.intent.action.VIEW -d "https://regacyviewl.onelink.me/zNHT/cdu65o1d" com.example.regacyviewexample
큰따옴표 안에 복사한 단축 URL 또는 긴 URL을 붙여넣고 바로 뒤엔 패키지 이름을 쓴다.
실행하면 터미널에 아래와 같은 텍스트들이 표시되면서 메인 액티비티가 새로 표시될 것이다.
adb shell am start -W -a android.intent.action.VIEW -d "https://regacyviewl.onelink.me/zNHT/cdu65o1d" com.example.regacyviewexample
Starting: Intent { act=android.intent.action.VIEW dat=https://regacyviewl.onelink.me/... pkg=com.example.regacyviewexample }
Status: ok
LaunchState: WARM
Activity: com.example.regacyviewexample/.presentation.MainActivity
TotalTime: 43
WaitTime: 48
Complete
Timber를 사용하고 로그를 안 지웠다면 로그캣엔 아래 로그가 표시된다.
## [handleDeepLink] intent : Intent { act=android.intent.action.VIEW dat=https://regacyviewl.onelink.me/... flg=0x10000000 pkg=com.example.regacyviewexample cmp=com.example.regacyviewexample/.presentation.MainActivity }, data: https://regacyviewl.onelink.me/zNHT/cdu65o1d
## [딥링크] deepLinkResult : {"deepLink":"{\"af_sub1\":\"test\",\"path\":\"\\\/zNHT\\\/cdu65o1d\",\"scheme\":\"https\",\"media_source\":\"SMS\",\"af_xp\":\"text\",\"link\":\"https:\\\/\\\/regacyviewl.onelink.me\\\/zNHT\\\/cdu65o1d\",\"host\":\"regacyviewl.onelink.me\",\"campaign\":\"캠페인1\",\"deep_link_sub1\":\"1\",\"deep_link_value\":\"id\",\"is_deferred\":false}","status":"FOUND"}
## [딥링크] deepLinkObj : {"af_sub1":"test","path":"\/zNHT\/cdu65o1d","scheme":"https","media_source":"SMS","af_xp":"text","link":"https:\/\/regacyviewl.onelink.me\/zNHT\/cdu65o1d","host":"regacyviewl.onelink.me","campaign":"캠페인1","deep_link_sub1":"1","deep_link_value":"id","is_deferred":false}, deepLinkValue : id, campaign : 캠페인1, mediaSource : SMS
제대로 왔는지 확인하기 위해 앞서 원링크를 만들기 전에 확인했던 링크 미리보기와 비교해 본다.
딥링크 값은 deep_link_value, 추가 딥링크 값은 deep_link_sub1로 표시된다고 했었다.
그 결과 실제로 앱에서 받은 원링크 안에 deep_link_value(deepLinkValue)는 id로 설정됐고, deep_link_sub1은 1로 오는 걸 볼 수 있다.
campaign이란 이름의 키에는 "캠페인1"이 매핑돼 있고 media_source(mediaSource)는 텍스트 투 앱을 선택했을 때 고른 SMS로 돼 있으며, link와 host도 정상적으로 설정돼 있다.
이후부턴 기존에 하던 것처럼 원링크에서 값을 빼와 원하는 처리를 구현하면 된다.
fun setupDeepLinkListener() {
AppsFlyerLib.getInstance().subscribeForDeepLink(object : DeepLinkListener {
override fun onDeepLinking(deepLinkResult: DeepLinkResult) {
Timber.d("## [딥링크] deepLinkResult : $deepLinkResult")
when (deepLinkResult.status) {
DeepLinkResult.Status.FOUND -> {
val deepLinkObj = deepLinkResult.deepLink
val deepLinkValue = deepLinkObj.deepLinkValue
val campaign = deepLinkObj.campaign
val mediaSource = deepLinkObj.mediaSource
val link = deepLinkObj.getStringValue("link")
val host = deepLinkObj.getStringValue("host")
val path = deepLinkObj.getStringValue("path")
val scheme = deepLinkObj.getStringValue("scheme")
val afSub1 = deepLinkObj.getStringValue("af_sub1")
val afXp = deepLinkObj.getStringValue("af_xp")
val deepLinkSub1 = deepLinkObj.getStringValue("deep_link_sub1")
val deepLinkSub2 = deepLinkObj.getStringValue("deep_link_sub2")
val deepLinkSub3 = deepLinkObj.getStringValue("deep_link_sub3")
val isDeferred = deepLinkObj.isDeferred
Timber.d("## [딥링크 상세] " +
"\ndeepLinkValue: $deepLinkValue" +
"\ncampaign: $campaign" +
"\nmediaSource: $mediaSource" +
"\nlink: $link" +
"\nhost: $host" +
"\npath: $path" +
"\nscheme: $scheme" +
"\naf_sub1: $afSub1" +
"\naf_xp: $afXp" +
"\ndeep_link_sub1: $deepLinkSub1" +
"\ndeep_link_sub2: $deepLinkSub2" +
"\ndeep_link_sub3: $deepLinkSub3" +
"\nis_deferred: $isDeferred")
lifecycleScope.launch {
binding.run {
tvTest.text = buildString {
append("=== 딥링크 파라미터 ===\n")
append("deepLinkValue: $deepLinkValue\n")
append("campaign: $campaign\n")
append("mediaSource: $mediaSource\n")
append("link: $link\n")
append("host: $host\n")
append("path: $path\n")
append("scheme: $scheme\n")
append("af_sub1: $afSub1\n")
append("af_xp: $afXp\n")
append("deep_link_sub1: $deepLinkSub1\n")
append("deep_link_sub2: $deepLinkSub2\n")
append("deep_link_sub3: $deepLinkSub3\n")
append("is_deferred: $isDeferred")
}
}
}
handleDeepLinkParameters(
deepLinkValue = deepLinkValue,
campaign = campaign,
mediaSource = mediaSource,
deepLinkSub1 = deepLinkSub1,
deepLinkSub2 = deepLinkSub2,
deepLinkSub3 = deepLinkSub3,
afSub1 = afSub1,
isDeferred = isDeferred
)
}
DeepLinkResult.Status.NOT_FOUND -> {
Timber.d("## [딥링크] 딥링크 없음")
}
DeepLinkResult.Status.ERROR -> {
Timber.e("## [딥링크] 에러 발생 : ${deepLinkResult.error}")
}
}
}
})
}
// 함수 매개변수는 필요한 값으로 수정한다
private fun handleDeepLinkParameters(
deepLinkValue: String?,
campaign: String?,
mediaSource: String?,
deepLinkSub1: String?,
deepLinkSub2: String?,
deepLinkSub3: String?,
afSub1: String?,
isDeferred: Boolean?,
) {
when (deepLinkValue) {
"id" -> {
Timber.d("## [딥링크] ID 관련 처리: deepLinkSub1=$deepLinkSub1")
// deepLinkSub1을 ID로 써서 특정 화면 이동
navigateToSpecificScreen(deepLinkSub1)
}
"product" -> {
Timber.d("## [딥링크] 상품 관련 처리: productId=$deepLinkSub1")
// 상품 페이지로 이동
}
"event" -> {
Timber.d("## [딥링크] 이벤트 관련 처리: eventId=$deepLinkSub1")
// 이벤트 페이지로 이동
}
}
campaign?.let {
when (it) {
"캠페인1" -> {
Timber.d("## [딥링크] 캠페인1 처리")
// 특정 캠페인 로직
}
}
}
mediaSource?.let {
when (it) {
"SMS" -> {
Timber.d("## [딥링크] SMS를 통한 유입")
}
"Facebook" -> {
Timber.d("## [딥링크] Facebook을 통한 유입")
}
}
}
if (isDeferred == true) {
Timber.d("## [딥링크] 디퍼드 딥링크 - 앱 설치 후 첫 실행")
// 웰컴 메시지 같은 로직 추가
} else {
Timber.d("## [딥링크] 일반 딥링크 - 앱이 이미 설치된 상태")
}
}
private fun navigateToSpecificScreen(id: String?) {
id?.let {
// 화면 이동 로직
}
}
서두에도 말했지만 위 코드는 예시 코드기 때문에 실제로 사용할 거라면 반드시 작동 여부를 확인한 후 리팩토링을 진행한다. 작동 여부 확인이 최우선이다.
또한 이 예시에선 텍스트뷰에 원링크 값들을 표시하기 위해 lifecycleScope.launch를 사용했지만 메인 쓰레드가 불필요하다면 굳이 쓸 필요 없다.
'Android' 카테고리의 다른 글
[Android] Flow 단위 테스트 라이브러리 Turbine 사용법 (0) | 2025.09.13 |
---|---|
[Android] 모듈 생성 시 Android Library, Java or Kotlin Library 선택 방법 (0) | 2025.09.11 |
[Android] AlarmManager란? 사용 예시 (0) | 2025.08.26 |
[Android] XML용 NumberPicker 커스텀 라이브러리 사용법 (0) | 2025.07.18 |
[Android] 다운로드 매니저(DownloadManager) 구현 방법 (0) | 2025.07.07 |