1 Foundation · 1-1

개발자 협업 가이드 및 테크니컬 PRD

MMP(Adjust) SDK 초기화 스펙, 딥링크 라우팅 로직, 그리고 릴리즈 전 QA 시나리오를 단일 문서로 통합한 개발 협업 표준. 본 문서가 그대로 PRD가 되도록 작성됨.

담당 · Growth Eng. Adjust SDK ≥ v5.0.0 iOS 14.5+ 필수
핵심 요약

본 문서는 신규 앱 또는 리뉴얼 앱의 어트리뷰션 기반 마련을 위해 개발팀이 구현해야 할 최소 필수 스펙을 정의한다. Adjust SDK v5 초기화 → 디퍼드 딥링크 라우팅 → 릴리즈 QA 체크리스트의 3단 게이트를 통과한 빌드만 LIVE 캠페인에 진입한다. 잘못 셋업된 SDK 한 줄이 전 캠페인 데이터를 무효화시키므로, 모든 항목은 코드 리뷰 + QA Lead 더블 사인오프를 강제한다.

§1사전 요구사항

  • Adjust 대시보드 권한: App Token (iOS/Android 분리), Environment(sandbox/production), Default Tracker 발급 완료
  • Bundle ID / Package Name 사전 등록. Android는 applicationId 기준, iOS는 App Store Connect의 Bundle Identifier 기준
  • StoreKit 2 + ATTrackingManager 의존성 및 iOS 14.5+ 지원
  • App Links / Universal Links 도메인 검증 파일 호스팅 (.well-known/apple-app-site-association, .well-known/assetlinks.json)

§2구현 단계

2.1 iOS · Adjust SDK 초기화 (Swift, AppDelegate)

초기화는 반드시 ATT 프롬프트 응답 후 진행한다. 프롬프트 이전 초기화 시 IDFA가 영구적으로 0으로 고정되어 attribution이 SKAdNetwork 단일 채널로 강제 다운그레이드된다.

iOS · AppDelegate.swift
import Adjust
      import AdjustSdk
      import AppTrackingTransparency

      func application(_ application: UIApplication,
                       didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

          let appToken = "abc1234xyz9"  class="c">// Production token
          let environment = ADJEnvironmentProduction

          let config = ADJConfig(appToken: appToken,
                                 environment: environment,
                                 allowSuppressLogLevel: false)
          config?.logLevel = ADJLogLevelInfo
          config?.needsCost = true                 class="c">// ROAS 계산용 비용 데이터
          config?.linkMeMetadata = true            class="c">// LinkMe 클립보드 매칭 활성화
          class="c">// attConsentWaitingInterval: Adjust가 ATT 응답을 기다리는 최대 시간(초).
          class="c">// 응답이 늦어도 첫 install/session 이벤트를 발화시키는 fallback 타이머이며,
          class="c">// ATT 프롬프트 자체의 표시 시간과는 무관 (App Store 가이드라인과 별개 영역).
          class="c">// 120초 = ATT 응답 확보율 최대화 우선 운영.
          config?.attConsentWaitingInterval = 120

          class="c">// SKAN 4.0 ConversionValue 업데이트 위임
          config?.delegate = SKANDelegateHandler.shared

          Adjust.appDidLaunch(config)

          class="c">// ATT 프롬프트는 사용자가 앱의 가치를 인지한 시점에 호출 (온보딩 완료 후 등).
          class="c">// 첫 실행 즉시 호출하면 거부율↑, 또한 너무 빠른 호출은 사용자 컨텍스트 부재로 reject 사유.
          class="c">// 1-4 가이드의 ATT 디자인 패턴 참고.
          OnboardingState.shared.onCompleted = {
              ATTrackingManager.requestTrackingAuthorization { _ in
                  Adjust.requestTrackingAuthorization { status in
                      class="c">// status: 0 Not Determined / 3 Authorized
                      print("[ATT] status=\(status)")
                  }
              }
          }
          return true
      }

2.2 Android · Adjust SDK 초기화 (Kotlin, Application 클래스)

Application 서브클래스에서 최상위 onCreate에 위치시켜야 한다. 외부 라이브러리(Firebase 등)보다 늦게 초기화될 경우, 첫 세션의 install referrer가 누락된다.

Android · App.kt
import android.app.Application
      import com.adjust.sdk.Adjust
      import com.adjust.sdk.AdjustConfig
      import com.adjust.sdk.LogLevel

      class App : Application() {
          override fun onCreate() {
              super.onCreate()

              val appToken = "abc1234xyz9"
              val environment = AdjustConfig.ENVIRONMENT_PRODUCTION

              val config = AdjustConfig(this, appToken, environment).apply {
                  setLogLevel(LogLevel.INFO)
                  setNeedsCost(true)
                  setPreinstallTrackingEnabled(true)        class="c">// 사전탑재 트래킹
                  setSendInBackground(true)                 class="c">// 백그라운드 전송
                  setEventBufferingEnabled(false)           class="c">// 실시간 캠페인 운영 시 false
                  setOnAttributionChangedListener { attr ->
                      class="c">// network_name / campaign_name / adgroup_name / creative_name
                      AppAnalytics.bindAttribution(attr)
                  }
                  setOnDeeplinkResponseListener { uri ->
                      DeeplinkRouter.handle(uri)            class="c">// 디퍼드 딥링크 라우팅
                      true
                  }
              }

              Adjust.onCreate(config)
              registerActivityLifecycleCallbacks(AdjustLifecycleCallbacks())
          }
      }

2.3 딥링크 라우팅 로직 (디퍼드 포함)

3가지 진입점을 단일 라우터로 통합한다: (a) Universal Link / App Link 진입, (b) 커스텀 URI 스킴, (c) Adjust 디퍼드 딥링크(미설치 상태에서 스토어 경유 후 첫 실행 시 발화). 라우터는 항상 scheme://host/path?params 표준형으로 정규화한 뒤 분기한다.

Android · DeeplinkRouter.kt
object DeeplinkRouter {
          fun handle(uri: Uri): Boolean {
              val path = uri.pathSegments.firstOrNull() ?: return openHome()
              return when (path) {
                  "product" -> openProduct(uri.lastPathSegment ?: return openHome())
                  "promo"   -> openPromo(uri.getQueryParameter("code"))
                  "invite"  -> openReferral(uri.getQueryParameter("ref"))
                  else      -> openHome()
              }.also {
                  Analytics.track("deeplink_opened", mapOf(
                      "deeplink_path"   to uri.path.orEmpty(),
                      "deeplink_source" to (uri.getQueryParameter("utm_source") ?: "direct"),
                      "is_deferred"     to (DeferredFlag.consume())
                  ))
              }
          }
      }

2.4 표준 URL 스키마

매체 트래커 URL은 Adjust Custom URL에 아래 스키마를 강제한다. 모든 매크로는 매체별 placeholder를 사용하며, 임의 추가는 불가하다.

Adjust Click URL Template
https://app.adjust.com/{tracker_token}
        ?campaign={campaign_name}
        &adgroup={adgroup_name}
        &creative={creative_name}
        &idfa={idfa}            // iOS 14.5+ 에서는 자동 0 처리
        &gps_adid={gps_adid}    // Google Play Services ADID
        &cost_type=cpc
        &cost_amount={cost}
        &cost_currency=USD
        &deeplink=myapp%3A%2F%2Fproduct%2F{product_id}
        &fallback=https%3A%2F%2Fwww.example.com%2Fproduct%2F{product_id}

§3QA 시나리오 (릴리즈 게이트)

아래 12개 시나리오를 모두 PASS해야 LIVE 빌드로 승인된다. 실패 1건 = 빌드 반려.

IDScenarioExpectedSeverityTool
QA-01신규 설치 → 첫 세션 발화install + session_start 1회씩 발화BlockerAdjust Testing Console
QA-02ATT 프롬프트 Authorized 케이스IDFA 비공백, attribution sandbox에 도달BlockerCharles Proxy
QA-03ATT Denied 케이스IDFA=00000... 0, SKAN 단일 채널 정상 동작BlockerConsole
QA-04Universal Link 콜드 스타트DeeplinkRouter.handle 1회 호출MajorSafari → 앱 전환
QA-05디퍼드 딥링크 (미설치 → 설치)최초 실행 시 onDeeplinkResponse 발화BlockerTest Console + Test Device
QA-06커스텀 URI 스킴 (외부 브라우저)Router 분기 정확Majoradb shell am start
QA-07백그라운드 ↔ 포그라운드 전환session 중복 발화 없음MajorAdjust Logs
QA-08비행기 모드 → 온라인 복귀이벤트 큐잉 후 1회만 전송MajorCharles
QA-09SKAN postback (Apple Test Mode)coarse value [low|medium|high] 매핑 정상Blockerxcrun simctl + Apple Console
QA-10Cost data 수신 (UAC/AAP)Adjust 대시보드에 cost_amount 노출MinorAdjust > Cost Data
QA-11in_app_purchase 매출 이벤트revenue + currency 정확BlockerStoreKit Test
QA-12Uninstall + Reinstallreattribution 분류 정상MajorAdjust > Reattribution

§4트러블슈팅 / 함정

!
install 이벤트가 중복 발화된다

Adjust.appDidLaunch가 Application과 AppDelegate 양쪽에서 호출되는 경우. Application 클래스에서 단 1회만 호출되도록 통일하고, ActivityLifecycleCallbacks는 register는 하되 SDK 초기화는 분리한다.

!
iOS에서 attribution이 organic으로만 잡힌다

1) ATT 프롬프트가 SDK 초기화 이후에 호출되었는지 확인. 2) attConsentWaitingInterval이 너무 짧으면 사용자가 응답하기 전 install이 noIDFA로 굳어버림 — 120초로 설정해 응답 윈도우 확보. 3) LinkMe 도메인이 매체별 클릭 URL에 일치하는지 확인.

!
디퍼드 딥링크가 첫 실행에 발화되지 않는다

Android: Play Install Referrer API 의존성 누락 가능성. com.android.installreferrer:installreferrer:2.2 추가. iOS: Universal Links의 apple-app-site-association가 HTTPS, Content-Type=application/json으로 서빙되는지 검증.

i
Sandbox에서 production 토큰을 잘못 사용 중

환경 분리는 BuildConfig.DEBUG 또는 #if DEBUG로 빌드 타임에 분기. Sandbox 토큰을 Production에 사용하면 raw event는 Adjust에 도달하지만 attribution은 발생하지 않음.

§5인수인계 체크리스트

QA Lead → Growth PM 인수인계 전 확인:

  • Adjust 대시보드 raw export 1일치 다운로드, 디버그 트래픽 분리 검증
  • SKAN postback 수신 endpoint 등록 (skadnetwork.adjust.com) 및 partner re-direct ON
  • Firebase / Amplitude 등 2nd-party 분석 툴과 user_id 키 정합성 확인
  • Adjust → BigQuery / S3 export 파이프라인 sync 확인