---
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
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]]