--- id: ios-universal-links-deep-linking title: iOS Universal Links — 웹 URL → 앱 진입 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [ios, deep-link, universal-link, vibe-coding] tech_stack: { language: "Swift", applicable_to: ["iOS"] } applied_in: [] aliases: [associated domains, AASA, custom URL scheme, deferred deep link] --- # iOS Universal Links > `https://example.com/order/123` → 앱 설치되어 있으면 앱이 처리, 아니면 웹. **AASA 파일 + Associated Domains capability** 필수. 옛 custom scheme (`myapp://`) 보다 안전. ## 📖 핵심 개념 - AASA (apple-app-site-association): 도메인이 어떤 앱과 연결. - Associated Domains capability: 앱에 도메인 등록. - onContinueUserActivity: SwiftUI / SceneDelegate. - Deferred deep link: 앱 설치 후 첫 실행에 의도 복원 — 별도 SDK (Branch, Adjust). ## 💻 코드 패턴 ### 1. 서버 — AASA ```json // https://example.com/.well-known/apple-app-site-association { "applinks": { "details": [ { "appIDs": ["TEAMID.com.example.MyApp"], "components": [ { "/": "/order/*" }, { "/": "/user/*", "?": { "ref": "?*" } }, { "/": "/admin/*", "exclude": true } ] } ] } } ``` 응답 헤더: `Content-Type: application/json`. 인증 / redirect X. ### 2. 앱 — Associated Domains - Xcode → Signing & Capabilities → Associated Domains. - `applinks:example.com` 추가. ### 3. SwiftUI — handle ```swift @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in guard let url = activity.webpageURL else { return } handle(url) } .onOpenURL { url in // Custom scheme (myapp://) 또는 system 호출 handle(url) } } } func handle(_ url: URL) { let comps = URLComponents(url: url, resolvingAgainstBaseURL: false) guard let path = comps?.path else { return } if path.hasPrefix("/order/") { let id = String(path.dropFirst("/order/".count)) navigationStore.navigate(.order(id)) } } } ``` ### 4. UIKit — SceneDelegate ```swift func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL { handle(url) } } func scene(_ scene: UIScene, openURLContexts contexts: Set) { if let url = contexts.first?.url { handle(url) } } ``` ### 5. Custom URL scheme (legacy) ```xml CFBundleURLTypes CFBundleURLSchemes myapp ``` `myapp://order/123` — but Universal Links 권장 (위조 가능 / 사용자 confusion). ### 6. Deferred deep link - 앱 미설치 → App Store 이동 → 설치 후 첫 실행 시 원래 의도 복원. - iOS 자체 지원 X — Branch.io / Adjust / AppsFlyer SDK. ## 🤔 의사결정 기준 | 상황 | 도구 | |---|---| | 외부 URL → 앱 진입 | Universal Links | | OS 가 호출 (인증 callback) | Universal Link 또는 Custom scheme | | Push notification 안 deep link | userInfo + handle() | | 위젯 → 앱 deep link | widgetURL | | 앱 설치 전 URL 의 의도 복원 | Deferred (외부 SDK) | | QR / NFC | App Clip + invocation URL | ## ❌ 안티패턴 - **AASA 가 redirect 또는 401**: 앱 인식 못 함. CDN / 로컬 호스팅 정확히. - **AASA 너무 큼 (1MB+)**: 다운로드 안 됨. 작게. - **Universal Link 가 같은 도메인의 사파리에서 호출**: 앱 안 열림. 다른 앱에서 들어와야. - **handle 가 navigation 만 — 인증 검증 누락**: 권한 우회 가능. - **스킴만 의존**: 위조 가능, 다른 앱이 같은 scheme 등록 가능. - **path / params 검증 없이 직접 사용**: SQL/XSS 인젝션 가능. ## 🤖 LLM 활용 힌트 - AASA + Associated Domains + onContinueUserActivity 3종. - path / params 검증 (zod-like) 후 navigation. ## 🔗 관련 문서 - [[iOS_App_Clips]] - [[iOS_Push_Notifications]]