본문 바로가기
Floney

[Floney] Android Appsflyer 앱링크 설정

by 박매트 2024. 11. 7.

플로니 앱에서는 두 개의 앱 링크를 사용한다.

  1. 가계부 초대하기 link
  2. 정산 내역 공유하기 link

 

Android 에서 Appsflyer로 앱 링크를 설정한 글이 비교적 없어 작성해본다.


앱 링크 간단 정리

: AppsFlyer의 원링크는 앱 설치 여부에 따라, 사용자에게 구글 플레이 스토어 또는 앱 내 특정 페이지로 안내한다.

 

사이드 프로젝트에서 딥링크까지 구현하는 경우는 별로 없는 건지도 궁금하긴 합니다만,

구현한 당시에는 보고 참고할 수 있는 글이 오직 1개였다.

 

https://dev-ej2.tistory.com/45

 

안드로이드 스튜디오 / Appsflyer onelink 적용방법

앱내 게시물 링크를 공유해서 링크를 클릭하면 해당 게시물로 들어오게 하기 위해서는 deeplink를 사용해야한다. Appsflyer onelink를 사용하여 구현하였다. 1. Appsflyer sdk 설치 1-1. 모듈단위 gradle dependen

dev-ej2.tistory.com

이 글 보고 참고하여 만들었다!

 

약 1년 전에 출시된 iOS 앱은 appsflyer를 사용하여 앱링크를 구현하였다.

그래서 android도 동일하게 appsflyer로 앱링크를 구현하도록 해주었다.

 

딥링크는 사용자를 앱의 특정 위치로 바로 이동시키는 링크이고,

앱링크는 Android에서 웹사이트 도메인과 연동하여 링크를 클릭했을 때 앱이 직접 열리도록 하는 방식이다.

 

Appsflyer에서 링크 등록하기

 

 

Experiences & Deep Linking -> Onelink management에 들어가면 앱링크를 생성하고 관리할 수 있다.

 

사실 여기까지는 이미 iOS 개발자 분이 다 appsflyer에 등록을 한 상태였다. 

 

여기서 각 link(초대하기 링크, 정산 내역 공유 링크)마다 short URL이 있다.

이 URL을 base로 parameter에 변수랑 값을 추가하여 링크를 만들어주는 것이다.

 

 

그리고 (중요한 점 🌟🌟🌟🌟🌟)

꼭 저 edit template를 눌러서, Android SHA256 를 추가해줘야 한다.

 

그래야 링크 클릭 시, 앱을 실행된다.

안드로이드에서 딥링크가 정상적으로 실행되려면, 앱이 요청을 보낸 도메인에 대한 소유권을 증명해야하기 때무닝다.

추가 안해주면, 자꾸 플레이스토어로 가서 앱이 존재하지 않는다고 뜬다. 이걸로 삽질을 했다.

 

 

앱에서 링크 공유 시, 링크가 어떻게 생성되냐면,

short URL/파라미터변수=필요한식별값

 

이런 식으로 링크를 생성해서 카카오톡이나 메세지로 공유하면 되는 것이다!!

 

식별값이 필요한 이유는 

  • 초대 화면인 경우 : 어떤 가계부로 초대되었는 지, 그리고 들어오게 하려면 가계부 id가 필요했다.
  • 정산 공유 화면인 경우 : 어떤 가계부의 정산 내역인 지, 정산 내역 id가 필요했다.

이런식으로 파라미터 값을 다르게 해서 분기처리를 할 수 있다.

 

but, 정산 공유 링크의 경우는 bookKey가 보내지게 되는데, 이건 외부로 노출되면 안되는 키라고 한다.

그래서 위에서 만들어진 url을 NaverShortUrl 로 변환해서 공유하고, NaverShortUrl를 클릭하면 네이버가 알아서 기존 링크로 풀어준다고 한다. (이건 다음 포스팅에서 확인!!)

링크 공유까지는 금방 이해할 수 있었다.

 

링크 클릭 시, 앱 실행 및 특정 화면으로 이동

그런데, 이 링크를 클릭하면 앱으로 들어와야하고, 각자 화면으로 어떻게 이동하는 거지..? 라는 의문이 들었다.

정답은 스플래시 화면에서 화면을 이동하도록 설정해주면 된다!

 

앱을 실행할 때는 처음에 스플래시 화면이 시작되게 되어있기 때문이다.

시작 시에, 1)앱링크 클릭해서 시작된 앱인지, 아니면 2) 직접 앱을 클릭해서 실행된 앱인지 판별을 해준다.

 

우선 manifest 파일에서 SplashActivity 부분에 인텐트를 추가해줘야 한다.

 

	<activity
            android:name=".view.splash.SplashActivity"
            android:theme="@style/SplashTheme"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="floney"/>
            </intent-filter>
        </activity>

 

 

onNewIntent에서 인텐트 처리

이 메서드는 액티비티가 실행 중일 때 들어오는 새 인텐트를 받아 처리한다. handleIntent를 호출해 액티비티를 재실행하지 않고도 딥링크 데이터를 받아서 처리할 수 있도록 했다.

override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        handleIntent(intent)
    }

 

 

handleIntent에서 딥링크 유무에 따른 기본 라우팅

들어온 인텐트에 딥링크 URL이 포함되어 있다면 해당 URL을 navigateToAppropriateActivity 메서드로 넘겨서 화면을 라우팅한다. 반면, 딥링크가 없을 경우 기본적으로 홈 화면(HomeActivity)으로 이동해 사용자가 앱의 메인 화면으로 접근할 수 있도록 설정했다. 이 과정에서 overridePendingTransition을 사용해 부드러운 화면 전환하도록 구현했다.

 

private fun handleIntent(intent: Intent) {
        val data: Uri? = intent.data
        if (data != null) {
            navigateToAppropriateActivity(data)
        } else {
            // 딥 링크가 없을 경우 홈 화면으로 이동
            val intent = Intent(this@SplashActivity, HomeActivity::class.java)
            startActivity(intent)
            if (Build.VERSION.SDK_INT >= 34) {
                overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, android.R.anim.fade_in, android.R.anim.fade_out)
            } else {
                overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
            }
        }
    }

 

캠페인별 라우팅을 처리하는 navigateToAppropriateActivity

캠페인 파라미터를 통해 어떤 화면으로 이동할지 결정한다.

  • floney_share 캠페인
    이 캠페인을 통해 접근한 경우, 사용자에게 유효한 액세스 토큰이 있는지 확인한다. 액세스 토큰이 없다면 inviteCode 값을 포함해 로그인 화면(LoginActivity)으로 이동해 로그인을 유도한다. 액세스 토큰이 유효한 경우 BookEntranceActivity로 이동해 초대 코드를 그대로 전달한다.
  • floney_settlement_share 캠페인
    이 캠페인도 비슷하게 처리한다. 액세스 토큰이 없으면 로그인 화면으로 이동하고, 유효할 경우 SettleUpActivity로 이동해 settlementId와 bookKey를 전달하여 특정 가계부 정보를 바로 확인할 수 있도록 했다.
private fun navigateToAppropriateActivity(data: Uri) {
        data?.let {
            val campaign = it.getQueryParameter("campaign")
            when (campaign) {
                "floney_share" -> {
                    if(sharedPreferenceUtil.getString("accessToken", "") == "") { // accessToken 유효 X
                        val inviteCode = it.getQueryParameter("inviteCode")
                        val intent = Intent(this@SplashActivity, LoginActivity::class.java)

                        // 데이터를 Intent에 추가
                        intent.putExtra("settlementId", it.getQueryParameter("inviteCode"))

                        startActivity(intent)
                        if (Build.VERSION.SDK_INT >= 34) {
                            overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, android.R.anim.fade_in, android.R.anim.fade_out)
                        } else {
                            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
                        }
                    }
                    else {
                        val inviteCode = it.getQueryParameter("inviteCode")
                        val intent = Intent(this@SplashActivity, BookEntranceActivity::class.java)

                        // 데이터를 Intent에 추가
                        intent.putExtra("settlementId", it.getQueryParameter("inviteCode"))

                        startActivity(intent)
                        if (Build.VERSION.SDK_INT >= 34) {
                            overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, android.R.anim.fade_in, android.R.anim.fade_out)
                        } else {
                            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
                        }
                    }

                }
                "floney_settlement_share" -> {
                    if(sharedPreferenceUtil.getString("accessToken", "") == "") { // accessToken 유효 X
                        val inviteCode = it.getQueryParameter("inviteCode")
                        val intent = Intent(this@SplashActivity, LoginActivity::class.java)

                        // 데이터를 Intent에 추가
                        intent.putExtra("settlementId", it.getQueryParameter("inviteCode"))

                        startActivity(intent)
                        if (Build.VERSION.SDK_INT >= 34) {
                            overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, android.R.anim.fade_in, android.R.anim.fade_out)
                        } else {
                            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
                        }
                    }
                    else {
                        val intent = Intent(this@SplashActivity, SettleUpActivity::class.java)

                        // 데이터를 Intent에 추가
                        intent.putExtra("settlementId", it.getQueryParameter("settlementId"))
                        intent.putExtra("bookKey", it.getQueryParameter("bookKey"))

                        startActivity(intent)
                        if (Build.VERSION.SDK_INT >= 34) {
                            overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, android.R.anim.fade_in, android.R.anim.fade_out)
                        } else {
                            overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
                        }
                    }

                }
            }
        }
    }