StageUp
모바일 SDK딥링크

Android

AdStage DeepLink 통합 가이드 (Android)

목차

  1. 개요
  2. 프로젝트 설정
  3. AndroidManifest.xml 설정
  4. Application 클래스 설정
  5. MainActivity 설정
  6. 딥링크 수신 처리
  7. 딥링크 생성
  8. 고급 기능
  9. 트러블슈팅

개요

AdStage DeepLink SDK는 다음 기능을 제공합니다:

  • 실시간 딥링크: URL Scheme, App Link를 통한 즉시 처리
  • 디퍼드 딥링크: 앱 설치 후 첫 실행 시 복원
  • 동적 딥링크 생성: 서버 API를 통한 추적 가능한 링크 생성
  • 어트리뷰션 추적: UTM 파라미터 기반 마케팅 분석

프로젝트 설정

Gradle 의존성 추가

settings.gradle.kts

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://repo.nbase.io/repository/nbase-releases") }
    }
}

build.gradle.kts (Module: app)

dependencies {
    implementation("io.nbase:adapter-adstage:3.0.+")
    
    // 필수 의존성
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    implementation("com.squareup.okhttp3:okhttp:4.11.0")
}

AndroidManifest.xml 설정

1. 권한 설정

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
    <!-- 필수 권한 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
    <!-- Install Referrer 권한 (디퍼드 딥링크용) -->
    <uses-permission android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE" />
    
    <application>
        <!-- ... -->
    </application>
</manifest>
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">
    
    <!-- 기본 런처 -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    
    <!-- URL Scheme 딥링크 (예: myapp://promo/summer) -->
    <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="myapp"
            android:host="promo" />
    </intent-filter>
    
    <!-- App Link (https://go.myapp.com/...) -->
    <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="go.myapp.com" />
    </intent-filter>
    
    <!-- AdStage 딥링크 도메인 (예: https://go.adstage.net/...) -->
    <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="go.adstage.net" />
    </intent-filter>
</activity>

3. launchMode 설정 중요 사항

<!-- 권장: singleTask -->
android:launchMode="singleTask"
 
<!-- singleTask 사용 시:
     - 기존 Activity가 있으면 재사용
     - onNewIntent()가 호출됨
     - Task 최상단으로 이동
-->

Application 클래스 설정

MyApplication.kt

package com.example.myapp
 
import android.app.Application
import io.nbase.adapter.adstage.AdStage
 
class MyApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        
        // AdStage SDK 초기화
        AdStage.initialize(
            context = this,
            apiKey = "your-api-key-here",
            serverUrl = "https://api.adstage.app" // 선택사항, 기본값 사용 가능
        )
        
        // 딥링크 리스너 설정
        setupDeeplinkListener()
    }
    
    private fun setupDeeplinkListener() {
        AdStage.setDeeplinkListener(object : io.nbase.adapter.adstage.models.DeeplinkListener {
            override fun onDeeplinkReceived(data: io.nbase.adapter.adstage.models.DeeplinkData) {
                android.util.Log.d("AdStage", """
                    ✅ 딥링크 수신
                    - Short Path: ${data.shortPath}
                    - Link ID: ${data.linkId}
                    - Source: ${data.source}
                    - Parameters: ${data.parameters}
                """.trimIndent())
                
                // 전역적으로 처리하거나 이벤트 버스로 전달
                handleGlobalDeeplink(data)
            }
            
            override fun onDeeplinkFailed(error: String, shortPath: String?) {
                android.util.Log.e("AdStage", """
                    ❌ 딥링크 실패
                    - Error: $error
                    - Short Path: $shortPath
                """.trimIndent())
            }
        })
    }
    
    private fun handleGlobalDeeplink(data: io.nbase.adapter.adstage.models.DeeplinkData) {
        // 전역 이벤트 버스나 SharedFlow를 통해 현재 Activity에 전달
        // 또는 딥링크 매니저에 저장 후 Activity에서 가져가기
    }
}

AndroidManifest.xml에 Application 등록

<application
    android:name=".MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/Theme.MyApp">
    <!-- ... -->
</application>

MainActivity 설정

MainActivity.kt

package com.example.myapp
 
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import io.nbase.adapter.adstage.AdStage
import io.nbase.adapter.adstage.models.DeeplinkData
import io.nbase.adapter.adstage.models.DeeplinkListener
 
class MainActivity : AppCompatActivity() {
    
    companion object {
        private const val TAG = "MainActivity"
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 딥링크 처리 (앱이 처음 실행되거나 백그라운드에서 실행될 때)
        handleIntent(intent)
        
        // 로컬 딥링크 리스너 설정 (선택사항)
        setupLocalDeeplinkListener()
    }
    
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        setIntent(intent) // 중요: 새 Intent로 교체
        
        Log.d(TAG, "onNewIntent called")
        
        // 딥링크 처리 (앱이 이미 실행 중일 때 새 딥링크 수신)
        handleIntent(intent)
    }
    
    private fun handleIntent(intent: Intent?) {
        if (intent == null) {
            Log.w(TAG, "Intent is null")
            return
        }
        
        Log.d(TAG, """
            Intent received:
            - Action: ${intent.action}
            - Data: ${intent.data}
            - Extras: ${intent.extras?.keySet()?.joinToString()}
        """.trimIndent())
        
        // AdStage SDK에 Intent 전달
        val handled = AdStage.handleIntent(this, intent)
        
        if (handled) {
            Log.i(TAG, "✅ AdStage가 딥링크를 처리했습니다")
        } else {
            Log.d(TAG, "ℹ️ AdStage 딥링크가 아닙니다")
            
            // 일반 Intent 처리
            handleRegularIntent(intent)
        }
    }
    
    private fun handleRegularIntent(intent: Intent) {
        when (intent.action) {
            Intent.ACTION_VIEW -> {
                // 일반 웹 링크나 커스텀 스킴 처리
                val uri = intent.data
                Log.d(TAG, "Regular deep link: $uri")
            }
            // 다른 액션 처리...
        }
    }
    
    /**
     * 로컬 딥링크 리스너 (Activity에서만 처리)
     * Application에서 전역 리스너를 설정했다면 선택사항
     */
    private fun setupLocalDeeplinkListener() {
        AdStage.setDeeplinkListener(object : DeeplinkListener {
            override fun onDeeplinkReceived(data: DeeplinkData) {
                Log.d(TAG, "📱 Activity에서 딥링크 수신: ${data.shortPath}")
                
                // 비즈니스 로직 처리
                when {
                    data.parameters.containsKey("campaign") -> {
                        handleCampaignDeeplink(data)
                    }
                    data.parameters.containsKey("promo") -> {
                        handlePromoDeeplink(data)
                    }
                    else -> {
                        handleDefaultDeeplink(data)
                    }
                }
            }
            
            override fun onDeeplinkFailed(error: String, shortPath: String?) {
                Log.e(TAG, "❌ 딥링크 실패: $error")
                // 에러 UI 표시
            }
        })
    }
    
    private fun handleCampaignDeeplink(data: DeeplinkData) {
        val campaign = data.parameters["campaign"]
        val channel = data.parameters["channel"]
        
        Log.d(TAG, """
            🎯 캠페인 딥링크 처리
            - Campaign: $campaign
            - Channel: $channel
        """.trimIndent())
        
        // 캠페인 화면으로 이동
        // startActivity(Intent(this, CampaignActivity::class.java).apply {
        //     putExtra("campaign", campaign)
        // })
    }
    
    private fun handlePromoDeeplink(data: DeeplinkData) {
        val promoCode = data.parameters["promo"]
        
        Log.d(TAG, "🎁 프로모션 코드: $promoCode")
        
        // 프로모션 적용
        // applyPromoCode(promoCode)
    }
    
    private fun handleDefaultDeeplink(data: DeeplinkData) {
        Log.d(TAG, "📋 기본 딥링크 처리: ${data.shortPath}")
        
        // 메인 화면 유지하거나 특정 화면으로 이동
    }
    
    override fun onDestroy() {
        super.onDestroy()
        
        // 리스너 정리 (메모리 누수 방지)
        // 전역 리스너를 사용한다면 여기서 clear하지 않음
        // AdStage.clearDeeplinkListener()
    }
}

딥링크 수신 처리

1. 실시간 딥링크 (앱 실행 중)

class MyActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        AdStage.setDeeplinkListener(object : DeeplinkListener {
            override fun onDeeplinkReceived(data: DeeplinkData) {
                // data.shortPath: "SDGWNBB"
                // data.linkId: "507f1f77bcf86cd799439011"
                // data.source: REALTIME 또는 DEFERRED
                // data.parameters: Map<String, String>
                
                // UTM 파라미터 추출
                val utmSource = data.parameters["utmSource"]
                val utmMedium = data.parameters["utmMedium"]
                val utmCampaign = data.parameters["utmCampaign"]
                
                // 커스텀 파라미터 추출
                val customParam = data.parameters["customKey"]
                
                // 화면 이동
                navigateToScreen(data)
            }
            
            override fun onDeeplinkFailed(error: String, shortPath: String?) {
                // 에러 처리
                showErrorDialog(error)
            }
        })
    }
}

2. 디퍼드 딥링크 (앱 설치 후 첫 실행)

// 자동으로 처리됨!
// AdStage.initialize() 시점에 Install Referrer를 자동으로 조회하고
// 저장된 딥링크가 있으면 onDeeplinkReceived가 자동 호출됨
 
// 수동 처리가 필요한 경우:
AdStage.handleInstallReferrer(context)

3. 딥링크 소스 구분

override fun onDeeplinkReceived(data: DeeplinkData) {
    when (data.source) {
        DeepLinkSource.REALTIME -> {
            Log.d(TAG, "🔗 실시간 딥링크 (앱 실행 중 수신)")
            // 즉시 화면 전환 가능
        }
        DeepLinkSource.INSTALL -> {
            Log.d(TAG, "📦 디퍼드 딥링크 (앱 설치 후 첫 실행)")
            // 온보딩 후 화면 전환 등
        }
        else -> {
            Log.d(TAG, "❓ 알 수 없는 소스")
        }
    }
}

딥링크 생성

1. Request 객체 방식

import io.nbase.adapter.adstage.AdStage
import io.nbase.adapter.adstage.models.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
 
class DeeplinkManager {
    
    fun createSimpleDeeplink() {
        CoroutineScope(Dispatchers.Main).launch {
            try {
                val request = CreateDeeplinkRequest(
                    name = "여름 프로모션 링크",
                    description = "2024년 여름 시즌 프로모션",
                    channel = "google-ads",
                    campaign = "summer2024",
                    parameters = mapOf(
                        "promo" to "SUMMER20",
                        "discount" to 20
                    )
                )
                
                val response = AdStage.createDeeplink(request)
                
                Log.d(TAG, """
                    ✅ 딥링크 생성 완료
                    - Short URL: ${response.shortUrl}
                    - Short Path: ${response.shortPath}
                    - ID: ${response.id}
                """.trimIndent())
                
                // 생성된 URL 공유
                shareUrl(response.shortUrl)
                
            } catch (e: Exception) {
                Log.e(TAG, "❌ 딥링크 생성 실패: ${e.message}")
            }
        }
    }
}

2. Builder 패턴 (DSL 스타일)

fun createDeeplinkWithBuilder() {
    CoroutineScope(Dispatchers.Main).launch {
        try {
            val response = AdStage.createDeeplink("겨울 프로모션") {
                description("2024-2025 겨울 시즌 프로모션")
                shortPath("WINTER24")  // 사용자 지정 경로
                
                // 트래킹 파라미터
                channel("facebook-ads")
                subChannel("instagram")
                campaign("winter2024")
                adGroup("fashion-lovers")
                creative("banner-001")
                content("hero-image")
                keyword("winter-sale")
                
                // 리다이렉트 설정
                redirectConfig {
                    type(RedirectType.APP)  // STORE, APP, WEB
                    
                    // Android 설정
                    android {
                        appScheme("myapp://promo/winter")
                        packageName("com.example.myapp")
                        webUrl("https://example.com/promo/winter")
                    }
                    
                    // iOS 설정
                    ios {
                        appScheme("myapp://promo/winter")
                        bundleId("com.example.myapp")
                        appStoreId("123456789")
                        webUrl("https://example.com/promo/winter")
                    }
                    
                    // 데스크톱 설정
                    desktop {
                        webUrl("https://example.com/promo/winter")
                    }
                }
                
                // 커스텀 파라미터
                parameter("discount", 30)
                parameter("promoCode", "WINTER30")
                parameter("validUntil", "2025-03-31")
                
                // 상태 설정
                status(DeeplinkStatus.ACTIVE)
            }
            
            Log.d(TAG, "✅ Short URL: ${response.shortUrl}")
            
        } catch (e: Exception) {
            Log.e(TAG, "❌ 에러: ${e.message}")
        }
    }
}

3. 콜백 방식 (비동기)

fun createDeeplinkWithCallback() {
    val request = CreateDeeplinkRequest(
        name = "앱 초대 링크",
        channel = "referral",
        campaign = "invite-friend",
        parameters = mapOf(
            "referrer" to "USER123",
            "bonus" to 5000
        )
    )
    
    // 콜백 방식은 suspend 함수를 코루틴으로 감싸서 사용
    CoroutineScope(Dispatchers.Main).launch {
        try {
            val response = AdStage.createDeeplink(request)
            onSuccess(response)
        } catch (e: Exception) {
            onError(e)
        }
    }
}
 
private fun onSuccess(response: CreateDeeplinkResponse) {
    Log.d(TAG, "딥링크 생성 성공: ${response.shortUrl}")
    
    // UI 업데이트
    runOnUiThread {
        textView.text = response.shortUrl
        shareButton.isEnabled = true
    }
}
 
private fun onError(error: Exception) {
    Log.e(TAG, "딥링크 생성 실패: ${error.message}")
    
    runOnUiThread {
        Toast.makeText(this, "딥링크 생성 실패", Toast.LENGTH_SHORT).show()
    }
}

4. RedirectType 설명

enum class RedirectType {
    STORE,  // 앱 미설치 시 → 스토어로 이동
            // 앱 설치 시 → 앱 실행 (디퍼드 딥링크)
    
    APP,    // 앱 미설치 시 → 웹 폴백 URL로 이동
            // 앱 설치 시 → 앱 실행 (실시간 딥링크)
    
    WEB     // 항상 웹 URL로 이동
            // 앱 설치 여부 무관
}

5. 딥링크 공유 예제

fun shareDeeplink(shortUrl: String) {
    val shareIntent = Intent(Intent.ACTION_SEND).apply {
        type = "text/plain"
        putExtra(Intent.EXTRA_TEXT, """
            🎁 특별 프로모션 초대!
            
            이 링크를 통해 가입하면 5,000원 할인 쿠폰을 드립니다.
            $shortUrl
        """.trimIndent())
    }
    
    startActivity(Intent.createChooser(shareIntent, "친구 초대하기"))
}

고급 기능

1. 전역 사용자 속성 설정

// Application onCreate()에서 설정
val userAttributes = UserAttributes(
    gender = "male",
    country = "KR",
    city = "Seoul",
    age = "28",
    language = "ko-KR"
)
AdStage.setUserAttributes(userAttributes)
 
// 이후 trackEvent 호출 시 자동으로 포함됨

2. 전역 디바이스 정보 설정

val deviceInfo = DeviceInfo(
    category = "mobile",
    platform = "Android",
    model = Build.MODEL,
    appVersion = BuildConfig.VERSION_NAME,
    osVersion = Build.VERSION.RELEASE
)
AdStage.setDeviceInfo(deviceInfo)

3. 사용자 ID 및 세션 관리

// 로그인 시
AdStage.setUserId("user_123456")
 
// 로그아웃 시
AdStage.setUserId(null)
 
// 새 세션 시작
AdStage.startNewSession()
 
// 현재 세션 ID 조회
val sessionId = AdStage.getSessionId()
Log.d(TAG, "Current Session: $sessionId")

4. 프로모션 배너 연동

// 프로모션 리스트 조회
CoroutineScope(Dispatchers.Main).launch {
    val params = PromotionListParams.builder()
        .bannerType("NATIVE")
        .region("KR")
        .limit(10)
        .build()
    
    val response = AdStage.getPromotionList(params)
    
    response.promotions.forEach { promotion ->
        Log.d(TAG, """
            프로모션: ${promotion.appName}
            배너: ${promotion.bannerUrl}
        """.trimIndent())
    }
}
 
// 프로모션 클릭 처리
CoroutineScope(Dispatchers.Main).launch {
    val result = AdStage.handlePromotionClick(context, promotion)
    
    when (result) {
        is PromotionClickResult.Success -> {
            Log.d(TAG, "스토어 열림: ${result.storeUrl}")
        }
        is PromotionClickResult.Failure -> {
            Log.e(TAG, "실패: ${result.error}")
        }
    }
}

트러블슈팅

1. 딥링크가 수신되지 않음

체크리스트:

  • AndroidManifest.xml에 intent-filter 올바르게 설정
  • android:exported="true" 설정 확인
  • android:launchMode="singleTask" 설정 권장
  • AdStage.initialize() 호출 확인
  • setDeeplinkListener() 호출 확인
  • handleIntent() 호출 확인

디버깅:

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    
    Log.d(TAG, """
        Intent Debug:
        - Action: ${intent?.action}
        - Data: ${intent?.data}
        - Scheme: ${intent?.data?.scheme}
        - Host: ${intent?.data?.host}
        - Path: ${intent?.data?.path}
    """.trimIndent())
    
    handleIntent(intent)
}

2. 디퍼드 딥링크가 작동하지 않음

원인:

  • Install Referrer 권한 없음
  • Google Play Store를 통하지 않은 설치 (APK 직접 설치)
  • Install Referrer API 초기화 실패

해결:

// 수동으로 Install Referrer 처리
AdStage.handleInstallReferrer(applicationContext)
 
// 로그 확인
Log.d(TAG, "Install Referrer 처리 완료")

확인 방법:

# 1. Digital Asset Links 파일 확인
https://go.myapp.com/.well-known/assetlinks.json
 
# 2. 검증 도구 사용
https://developers.google.com/digital-asset-links/tools/generator
 
# 3. ADB로 테스트
adb shell am start -a android.intent.action.VIEW -d "https://go.myapp.com/ABCDEF"

assetlinks.json 예제:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": [
      "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
    ]
  }
}]

4. ProGuard/R8 난독화 문제

proguard-rules.pro:

# AdStage SDK
-keep class io.nbase.adapter.adstage.** { *; }
-keepclassmembers class io.nbase.adapter.adstage.** { *; }

# 모델 클래스
-keep class io.nbase.adapter.adstage.models.** { *; }

# OkHttp
-dontwarn okhttp3.**
-keep class okhttp3.** { *; }

# Kotlin Coroutines
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}

5. 멀티 프로세스 환경

<!-- 별도 프로세스에서 실행되는 Activity가 있는 경우 -->
<activity
    android:name=".SomeActivity"
    android:process=":separate">
    <!-- 이 Activity에서도 딥링크를 처리하려면 별도 초기화 필요 -->
</activity>
// 각 프로세스에서 AdStage.initialize() 호출 필요
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // 모든 프로세스에서 초기화
        AdStage.initialize(this, apiKey)
    }
}

테스트 방법

1. ADB 테스트

# URL Scheme 테스트
adb shell am start -a android.intent.action.VIEW -d "myapp://promo/summer"
 
# App Link 테스트
adb shell am start -a android.intent.action.VIEW -d "https://go.myapp.com/ABCDEF"
 
# 파라미터 포함
adb shell am start -a android.intent.action.VIEW -d "myapp://promo?campaign=summer&discount=20"

2. Intent 로그 확인

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    
    intent?.let {
        Log.d(TAG, "=== Intent Debug ===")
        Log.d(TAG, "Action: ${it.action}")
        Log.d(TAG, "Data: ${it.data}")
        Log.d(TAG, "Extras: ${it.extras?.keySet()?.joinToString()}")
        
        it.data?.let { uri ->
            Log.d(TAG, "URI Scheme: ${uri.scheme}")
            Log.d(TAG, "URI Host: ${uri.host}")
            Log.d(TAG, "URI Path: ${uri.path}")
            Log.d(TAG, "URI Query: ${uri.query}")
        }
    }
}

3. 실제 디바이스 테스트

// 테스트용 딥링크 생성
CoroutineScope(Dispatchers.Main).launch {
    val response = AdStage.createDeeplink("테스트 링크") {
        channel("test")
        campaign("test-campaign")
        parameter("test", "true")
    }
    
    Log.d(TAG, "테스트 URL: ${response.shortUrl}")
    
    // URL을 복사하여 브라우저나 메신저에서 테스트
}

참고 자료


목차