Android/Kotlin

[Android/Kotlin] 딥 링크(deep links) 처리하기

양심고백 2026. 4. 30. 21:48
반응형
SMALL

 

딥 링크란?

  • 딥 링크란 외부 URL이나 알림 등에서 앱 내 특정 화면으로 직접 진입할 수 있게 해주는 메커니즘
  • 단순히 앱을 여는 것이 아니라 원하는 목적지까지 한 번에 도달하게 해준다.

 

사례 트리거 목적
OAuth 콜백 외부 앱/브라우저 인증 완료 후 앱 복귀
이메일 인증 이메일/문자 링크 토큰 수신 및 자동 로그인
푸시 알림 FCM 알림 클릭 특정 화면 직접 진입
SNS 공유 카카오톡/인스타 등 콘텐츠/상품 화면 공유
QR 코드 카메라 스캔 오프라인→온라인 연결

 

 

 

1. XML 방식

1-1. 매니페스트 선언

딥 링킹을 활성화하려면 딥 링크를 처리해야 하는 Activity에 대해 AndroidManifest.xml에서 intent filter를 선언해야 한다.

  • android:scheme: URL 스키마(가령, https)를 지정합니다.
  • android:host: 도메인(가령, example.com)을 지정합니다.
  • android:pathPrefix: URL의 경로(가령, /deepLink)를 정의합니다.
    이 설정을 통해 https://example.com/deepLink와 같은 URL이 액티비티를 열도록 허용합니다.
<!-- AnimalDiary - AndroidManifest.xml -->

<activity
    android:name=".ui.login.AuthCallbackActivity"
    android:exported="true"
    android:launchMode="singleTask">
    <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:host="auth"
            android:scheme="animaldiary" />
    </intent-filter>
</activity>

animaldiary://auth 형태의 URL이 들어오면 AuthCallbackActivity가 실행.

android:launchMode="standard" 모드라면 딥 링크가 들어올 때마다 Activity 인스턴스가 새로 쌓이는데, "singleTask”로 설정하면 이미 인스턴스가 존재할 경우 새로 만들지 않고 기존 인스턴스를 재사용.

 

1-2. Activity에서 직접 처리

// AnimalDiary - AuthCallbackActivity.kt

@AndroidEntryPoint
class AuthCallbackActivity : AppCompatActivity() {

    @Inject
    lateinit var tokenManager: TokenManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ① intent.data에서 쿼리 파라미터 추출
        val token = intent?.data?.getQueryParameter("token")

        if (!token.isNullOrEmpty()) {
            lifecycleScope.launch {
                // ② 토큰을 DataStore에 비동기 저장
                tokenManager.saveToken(token)

                // ③ 저장 완료 후 MainActivity로 이동, 백 스택 초기화
                val mainIntent = Intent(
                    this@AuthCallbackActivity,
                    MainActivity::class.java
                ).apply {
                    flags = Intent.FLAG_ACTIVITY_NEW_TASK or
                            Intent.FLAG_ACTIVITY_CLEAR_TASK
                }
                startActivity(mainIntent)
            }
        } else {
            // ④ 유효하지 않은 딥 링크 → 조용히 종료 (폴백 처리)
            finish()
        }
    }
}

 

2. Compose 방식

2-1. 매니페스트 선언

<!-- Kiero - AndroidManifest.xml -->

<activity
    android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
    android:exported="true">
    <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:host="oauth"
            android:scheme="kakao${NATIVE_APP_KEY}" />
    </intent-filter>
</activity>

이전 프로젝트와 달리 SAA구조이기에 직접 Activity를 만들지 않음.
카카오 SDK가 제공하는 AuthCodeHandlerActivity를 매니페스트에 등록하는 것만으로 OAuth 콜백 처리 위임. kakaoXXXXXX://oauth 형태의 URL이 들어오면 SDK가 알아서 인증 코드를 파싱하고 결과를 앱으로 돌려준다.

 

2-2. Application: SDK 초기화

// KieroApplication.kt

@HiltAndroidApp
class KieroApplication : Application(), ImageLoaderFactory {

    override fun onCreate() {
        super.onCreate()
        initKakaoSdk()
    }

    private fun initKakaoSdk() {
        try {
            KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY)
            Timber.tag("KAKAO_INIT").d("✅ 카카오 SDK 초기화 성공")
        } catch (e: Exception) {
            Timber.tag("KAKAO_INIT").e(e, "❌ 카카오 SDK 초기화 실패")
        }
    }
}

Application.onCreate()에서 SDK를 초기화해두어야 이후 딥 링크로 AuthCodeHandlerActivity가 열렸을 때 SDK가 정상 동작한다. 초기화 없이 콜백을 받으면 처리할 수 없으므로, 딥 링크보다 반드시 먼저 실행되어야 하는 코드다.

 

2-3. ViewModel: 로그인 로직과 상태 관리

// AuthParentViewModel.kt

fun loginWithKakao(context: Context) = viewModelScope.launch {
    _state.update { it.copy(uiState = UiState.Loading) }

    authRepository.loginWithKakao(context) // 내부적으로 카카오 SDK 호출
        .onSuccess { result ->
            handleKakaoLoginResultUseCase(
                name = result.name,
                image = result.image
            ).onSuccess { kakaoLoginResult ->
                when (kakaoLoginResult) {
                    is KakaoLoginResult.HasChildren ->
                        _sideEffect.emit(AuthSideEffect.NavigateToParentGraph)
                    is KakaoLoginResult.NoChildren ->
                        _sideEffect.emit(AuthSideEffect.NavigateToParentSignUp)
                }
            }
        }
        .onFailure { throwable ->
            // 사용자가 직접 취소한 경우를 별도 분기
            if (throwable is ClientError &&
                throwable.reason == ClientErrorCause.Cancelled) {
                handleError(message = "로그인이 취소되었습니다")
                return@onFailure
            }
            handleError(throwable)
        }
}

카카오 SDK가 딥 링크 콜백을 처리하고 결과를 반환하면, ViewModel이 그 결과에 따라 SideEffect로 네비게이션 이벤트를 emit한다.

사용자가 카카오 로그인 창을 직접 닫은 경우, ClientErrorCause.Cancelled로 구분해 별도 메시지를 보여주었다.

 

2-4. Composable: SideEffect 수신 및 화면 전환

// AuthParentScreen.kt

viewModel.sideEffect.collectSingleEvent {
    when (it) {
        AuthSideEffect.NavigateToParentGraph    -> navigateToParentGraph()
        is AuthSideEffect.NavigateToParentSignUp -> navigateToParentSignUp()
        AuthSideEffect.NavigateToSelection       -> navigateToSelection()
        is AuthSideEffect.ShowSnackbar -> {
            globalTrigger.showSnackbar(
                SnackbarState(message = it.message, bottomPadding = 110)
            )
        }
        else -> {}
    }
}

Composable은 ViewModel의 SideEffect를 구독하고, 이벤트 종류에 따라 네비게이션 함수를 호출한다.
UI는 "어디로 갈지"만 실행하고, 비즈니스 로직은 ViewModel에 있다.

 

3. 테스트: ADB로 딥 링크 시뮬레이션

실제 링크를 기기에서 열어볼 필요 없이, 터미널에서 바로 테스트할 수 있다.

# AnimalDiary 딥 링크 테스트
adb shell am start -a android.intent.action.VIEW \\
  -d "animaldiary://auth?token=test_token_123" \\
  com.animaldiary.app

# 카카오 OAuth 콜백 테스트
adb shell am start -a android.intent.action.VIEW \\
  -d "kakao{NATIVE_APP_KEY}://oauth?code=auth_code_here" \\
  com.kiero

올바르게 설정되었다면 대상 Activity가 열리며 Intent 데이터가 파싱된다.

 

 

 

4. 추가 고려 사항

4-1 . 커스텀 스키마 vs HTTPS

앱 내부에서만 쓰는 딥 링크라면 myapp:// 같은 커스텀 스키마로 충분하다.
다만 앱이 설치되지 않은 기기에서는 브라우저가 myapp://을 해석하지 못해 "웹페이지를 찾을 수 없음" 오류가 뜬다.

외부 사용자를 대상으로 한다면 https:// 스키마가 더 안전하다.
앱이 설치되어 있으면 앱이 열리고, 없으면 웹 브라우저나 플레이스토어로 자연스럽게 유도할 수 있기 때문이다.

두 프로젝트 모두 커스텀 스키마(animaldiary://, kakaoXXX://)를 사용하고 있는데, 이는 앱↔앱 간 OAuth 콜백처럼 앱이 반드시 설치된 환경을 전제로 하는 내부 흐름이므로 적절한 선택이다.

 

 

4-2. 폴백 처리 (Fallback Handling)

딥 링크로 들어온 데이터가 유효하지 않거나 파라미터가 불완전한 경우를 반드시 처리해야 한다. 
로그인 화면으로 보내거나 에러 메시지를 보여주는 것이 더 나은 UX다.

 

 

4-3. 내비게이션과 백 스택

딥 링크로 특정 화면에 바로 진입했을 때, 사용자가 뒤로 가기를 누르면 앱이 바로 종료되는 경우가 있다.

백 스택에 아무것도 없기 때문이다.

예를 들어 카카오톡 공유 링크로 상품 화면에 들어왔다가 뒤로 가기를 눌렀을 때 앱이 꺼진다면 최악의 UX다.
이를 해결하려면 해당 화면 아래에 홈 화면을 인위적으로 깔아두어야 한다.

 

 

4-4. App Links

https:// 딥 링크를 사용할 경우, 클릭 시 브라우저 선택 창이 뜰 수 있다.
App Links를 설정하면 브라우저를 거치지 않고 앱에서 직접 열린다. 

 

 

 

 

 

 

 

 

 

animal diary

GitHub is where animal diary builds software.

github.com

 

Team-Kiero

Team-Kiero has 4 repositories available. Follow their code on GitHub.

github.com

 

 

딥 링크 만들기  |  App architecture  |  Android Developers

Android에서 딥 링크를 구현하고 테스트하여 웹브라우저, 알림, 광고와 같은 다양한 외부 소스에서 앱으로 직접 이동할 수 있도록 합니다.

developer.android.com

 

GitHub - skydoves/manifest-android-interview: 🚀 Manifest Android Interview is the ultimate guide to cracking Android technica

🚀 Manifest Android Interview is the ultimate guide to cracking Android technical interviews. - skydoves/manifest-android-interview

github.com

 

반응형
LIST