--- id: mobile-deep-link-verification title: Deep Link Verification — Universal / App Links / 검증 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [mobile, deep-link, universal-link, app-link, vibe-coding] tech_stack: { language: "Swift / Kotlin", applicable_to: ["iOS", "Android"] } applied_in: [] aliases: [App Links, Universal Links, AASA, assetlinks, deep link verification, deferred deep link] --- # Deep Link Verification > Web URL → app. **iOS = Universal Links + AASA, Android = App Links + assetlinks.json**. 검증 안 되면 browser 가 처리. Deferred = install 후 의도 복원. ## 📖 핵심 개념 - Verification: 서버 가 owns this domain. - AASA (iOS): apple-app-site-association. - assetlinks (Android): .well-known/assetlinks.json. - Deferred: install 후 의도 복원 (Branch / Adjust). ## 💻 코드 패턴 ### iOS — AASA ```json // https://example.com/.well-known/apple-app-site-association // (HTTP/2 + HTTPS + 정확 Content-Type: application/json) { "applinks": { "details": [ { "appIDs": ["TEAMID.com.example.app"], "components": [ { "/": "/order/*" }, { "/": "/user/*", "?": { "ref": "?*" } }, { "/": "/admin/*", "exclude": true } ] } ] }, "webcredentials": { "apps": ["TEAMID.com.example.app"] } } ``` ### iOS — Associated Domains ``` Xcode → Signing & Capabilities → Associated Domains + applinks:example.com + webcredentials:example.com # password autofill ``` ```swift // SwiftUI handle .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in guard let url = activity.webpageURL else { return } handle(url) } .onOpenURL { url in handle(url) } func handle(_ url: URL) { let comps = URLComponents(url: url, resolvingAgainstBaseURL: false) guard let path = comps?.path else { return } // ⚠️ Path / params 검증 if path.hasPrefix("/order/") { let id = String(path.dropFirst("/order/".count)) guard isValidUuid(id) else { return } navigationStore.navigate(.order(id)) } } ``` ### Android — assetlinks.json ```json // https://example.com/.well-known/assetlinks.json [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.app", "sha256_cert_fingerprints": ["AA:BB:CC:..."] } } ] ``` → SHA256 = signing cert. release / debug 둘 다 포함 가능. ```bash # Cert fingerprint 추출 keytool -list -v -keystore my-release-key.keystore # 또는 Play Console — App signing ``` ### Android — Manifest ```xml ``` → `android:autoVerify="true"` = OS 가 install 시 verify. ### Android — handle ```kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) intent?.data?.let(::handleDeepLink) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) intent.data?.let(::handleDeepLink) } private fun handleDeepLink(uri: Uri) { val path = uri.path ?: return if (path.startsWith("/order/")) { val id = path.removePrefix("/order/") navigate(Screen.Order(id)) } } } ``` ### Verification check ```bash # iOS swcutil verify --domain example.com --bundle-identifier com.example.app # Android adb shell pm get-app-links com.example.app # 또는 Play Console → App Links # Apple validator https://branch.io/resources/aasa-validator/ ``` ### 검증 실패 원인 ``` iOS: - AASA 가 redirect / 401 - Content-Type 가 application/json X - 잘못된 appID (TeamID 정확) - 옛 cache (다음 install 까지 wait) Android: - assetlinks.json HTTP / 잘못 - SHA256 fingerprint 불일치 (debug vs release) - pathPattern 불일치 - autoVerify 안 한 channel ``` ### Cache 무효화 ```bash # iOS xcrun simctl openurl booted https://example.com/test # Android — clear app links adb shell pm clear-app-links com.example.app adb shell pm verify-app-links --re-verify com.example.app ``` ### Deferred deep link (install 후 의도 복원) ``` 사용자가 ad → install → first launch. 원래 URL 의 의도 (특정 product) 복원. iOS / Android 자체 X — 외부 SDK: - Branch.io - Adjust - AppsFlyer - Singular ``` ```ts // Branch import branch from 'react-native-branch'; const { params } = await branch.getLatestReferringParams(); if (params.product_id) navigation.navigate('Product', { id: params.product_id }); ``` ### Custom URL scheme (legacy) ```xml CFBundleURLTypes CFBundleURLSchemes myapp ``` ```kotlin ``` → `myapp://order/123` — but Universal/App Links 권장. Custom scheme 위조 가능. ### 보안 — Path / params 검증 ```swift func handle(_ url: URL) { // 1. Whitelist host guard url.host == "example.com" else { return } // 2. Path validation guard let path = url.path.split(...) else { return } // 3. Param sanitization guard let id = url.queryItems["id"], isValidUuid(id) else { return } // 4. Auth — link 가 login 우회 X if requiresLogin(path) && !isLoggedIn() { navigateAfterLogin(.path(path)) return } navigate(...) } ``` ### Notification + deep link ```ts // Push payload { aps: { alert: { ... } }, link: 'https://example.com/order/42' } ``` ```swift // Tap notification → openURL let link = userInfo["link"] as? String if let url = URL(string: link) { handle(url) } ``` ### Web fallback ```html Order 42

Open in app

Open in app Install on iOS Install on Android ``` ### App Clip (iOS) / Instant App (Android) ``` 사용자가 install 안 해도 app 의 작은 부분 실행. QR / NFC / link 로. ``` → [[iOS_App_Clips]]. ### Test ```ts // Maestro - launchApp: arguments: url: "https://example.com/order/42" # 또는 adb adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/order/42" com.example.app # iOS xcrun simctl openurl booted "https://example.com/order/42" ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | 일반 deep link | Universal Links (iOS) + App Links (Android) | | Marketing campaign | + Deferred (Branch / Adjust) | | Private (auth callback) | Custom scheme (잠시 OK) | | App Clip / Instant App | 가벼운 entry point | | Notification 안 link | Universal Link 또는 custom scheme | ## ❌ 안티패턴 - **AASA / assetlinks 가 redirect**: verification 깨짐. - **SHA256 fingerprint 불일치 (release / debug 다름)**: 옛 release 가 안 됨. - **Path / params 검증 X**: SQL / XSS injection. - **Deep link 가 auth 우회**: 인증 검사 매번. - **Custom scheme 만 의존**: 위조 가능. - **Wildcard path (`*`)**: 의도 외 link. - **Web fallback 없음**: 미설치 시 dead link. ## 🤖 LLM 활용 힌트 - AASA + assetlinks + Associated Domains + autoVerify 4종. - Path / params validate (zod-style). - Auth check 매번. - Deferred = Branch / Adjust. ## 🔗 관련 문서 - [[iOS_Universal_Links_Deep_Linking]] - [[Android_Bluetooth_LE_Scanning]] - [[Mobile_Push_Deep]]