Files
2nd/10_Wiki/Topics/Coding/iOS_Universal_Links_Deep_Linking.md
T
2026-05-09 21:08:02 +09:00

4.3 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
ios-universal-links-deep-linking iOS Universal Links — 웹 URL → 앱 진입 Coding draft B conceptual 2026-05-09 2026-05-09
ios
deep-link
universal-link
vibe-coding
language applicable_to
Swift
iOS
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

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

@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

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
       let url = userActivity.webpageURL { handle(url) }
}

func scene(_ scene: UIScene, openURLContexts contexts: Set<UIOpenURLContext>) {
    if let url = contexts.first?.url { handle(url) }
}

5. Custom URL scheme (legacy)

<!-- Info.plist -->
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array><string>myapp</string></array>
  </dict>
</array>

myapp://order/123 — but Universal Links 권장 (위조 가능 / 사용자 confusion).

  • 앱 미설치 → 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.

🔗 관련 문서