모바일 SDK딥링크
iOS
AdStage DeepLink 통합 가이드 (iOS)
목차
개요
AdStage DeepLink SDK는 다음 기능을 제공합니다:
- 실시간 딥링크: URL Scheme, Universal Links를 통한 즉시 처리
- 디퍼드 딥링크: 앱 설치 후 첫 실행 시 자동 복원
- 동적 딥링크 생성: 서버 API를 통한 추적 가능한 링크 생성
- 어트리뷰션 추적: UTM 파라미터 기반 마케팅 분석
프로젝트 설정
CocoaPods 설정
Podfile
platform :ios, '12.0'
target 'YourApp' do
use_frameworks!
# AdStage SDK
pod 'AdapterAdStage', '3.0.5'
# 의존성 (자동 설치됨)
# Alamofire, SwiftyJSON 등
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
end
end
end설치:
pod installInfo.plist 설정
1. URL Scheme 설정
Info.plist에 추가:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>2. Universal Links 설정
Info.plist에 추가:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:go.myapp.com</string>
<string>applinks:go.adstage.net</string>
</array>Xcode 설정:
- Signing & Capabilities 탭 선택
- + Capability 클릭
- Associated Domains 추가
- 도메인 추가:
applinks:go.myapp.comapplinks:go.adstage.net
3. Apple App Site Association 파일
서버에 배포: https://go.myapp.com/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.com.example.myapp",
"paths": ["*"]
}
]
}
}주의사항:
- 파일 확장자 없음 (
.json붙이지 말 것) - Content-Type:
application/json - HTTPS 필수
- 루트 경로 또는
.well-known폴더에 배치
4. 필수 권한 설정
<!-- 네트워크 통신 -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
</dict>
<!-- 백그라운드 모드 (선택사항) -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>AppDelegate 설정
Swift - AppDelegate.swift
import UIKit
import AdapterAdStage
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// MARK: - Application Lifecycle
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 1. AdStage SDK 초기화
initializeAdStage()
// 2. 딥링크 리스너 설정
setupDeepLinkListener()
return true
}
// MARK: - AdStage Initialization
private func initializeAdStage() {
AdStageManager.shared.initialize(
apiKey: "your-api-key-here",
serverUrl: "https://api.adstage.app" // 선택사항
)
print("✅ AdStage SDK 초기화 완료")
}
private func setupDeepLinkListener() {
// 통합 딥링크 리스너 설정
DeepLinkManager.shared.setUnifiedListener { [weak self] deepLinkData in
guard let self = self else { return }
print("""
📱 딥링크 수신:
- Short Path: \(deepLinkData.shortPath)
- Link ID: \(deepLinkData.linkId)
- Source: \(deepLinkData.source.description)
- Parameters: \(deepLinkData.parameters)
""")
// 딥링크 처리
self.handleDeepLink(deepLinkData)
}
}
// MARK: - Deep Link Handling
private func handleDeepLink(_ data: DeepLinkData) {
// 소스별 처리
switch data.source {
case .realtime:
print("🔗 실시간 딥링크 처리")
handleRealtimeDeepLink(data)
case .install:
print("📦 디퍼드 딥링크 처리 (앱 설치 후 첫 실행)")
handleDeferredDeepLink(data)
case .unknown:
print("❓ 알 수 없는 딥링크 소스")
}
}
private func handleRealtimeDeepLink(_ data: DeepLinkData) {
// 파라미터 기반 화면 이동
if let campaign = data.parameters["campaign"] {
navigateToCampaign(campaign)
} else if let promo = data.parameters["promo"] {
navigateToPromotion(promo)
} else {
navigateToHome()
}
}
private func handleDeferredDeepLink(_ data: DeepLinkData) {
// 온보딩 후 처리 또는 즉시 처리
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.handleRealtimeDeepLink(data)
}
}
// MARK: - Navigation
private func navigateToCampaign(_ campaign: String) {
print("🎯 캠페인 화면으로 이동: \(campaign)")
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let campaignVC = storyboard.instantiateViewController(
withIdentifier: "CampaignViewController"
) as? CampaignViewController {
campaignVC.campaignId = campaign
if let navController = self.window?.rootViewController as? UINavigationController {
navController.pushViewController(campaignVC, animated: true)
}
}
}
}
private func navigateToPromotion(_ promo: String) {
print("🎁 프로모션 적용: \(promo)")
// 프로모션 처리 로직
}
private func navigateToHome() {
print("🏠 홈 화면으로 이동")
// 홈 화면 이동
}
// MARK: - URL Scheme Deep Link (iOS 8+)
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
print("📲 URL Scheme 딥링크 수신: \(url)")
// AdStage에서 처리
let handled = DeepLinkManager.shared.handleDeepLink(from: url)
if handled {
print("✅ AdStage가 딥링크를 처리했습니다")
return true
}
// 다른 URL Scheme 처리 (예: OAuth 콜백)
if url.scheme == "myapp" {
// 커스텀 처리
return handleCustomScheme(url)
}
return false
}
// MARK: - Universal Links (iOS 9+)
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
// Universal Link인지 확인
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
print("🌐 Universal Link 수신: \(url)")
// AdStage에서 처리
let handled = DeepLinkManager.shared.handleUniversalLink(userActivity: userActivity)
if handled {
print("✅ AdStage가 Universal Link를 처리했습니다")
return true
}
// 다른 Universal Link 처리
return handleCustomUniversalLink(url)
}
// MARK: - Custom Handlers
private func handleCustomScheme(_ url: URL) -> Bool {
print("🔧 커스텀 URL Scheme 처리: \(url)")
// OAuth, 결제 콜백 등 처리
return false
}
private func handleCustomUniversalLink(_ url: URL) -> Bool {
print("🔧 커스텀 Universal Link 처리: \(url)")
// 일반 웹 링크 처리
return false
}
}SwiftUI - App 구조
import SwiftUI
import AdapterAdStage
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
// URL Scheme 처리
print("📲 URL 수신: \(url)")
_ = DeepLinkManager.shared.handleDeepLink(from: url)
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
// AdStage 초기화
AdStageManager.shared.initialize(
apiKey: "your-api-key-here",
serverUrl: "https://api.adstage.app"
)
// 딥링크 리스너 설정
setupDeepLinkListener()
return true
}
private func setupDeepLinkListener() {
DeepLinkManager.shared.setUnifiedListener { deepLinkData in
print("📱 딥링크 수신: \(deepLinkData.shortPath)")
// 처리 로직
}
}
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
// Universal Link 처리
return DeepLinkManager.shared.handleUniversalLink(userActivity: userActivity)
}
}딥링크 수신 처리
1. 실시간 딥링크 처리
import AdapterAdStage
class DeepLinkHandler {
static let shared = DeepLinkHandler()
func setupListener() {
DeepLinkManager.shared.setUnifiedListener { [weak self] deepLinkData in
guard let self = self else { return }
// 딥링크 데이터 추출
let shortPath = deepLinkData.shortPath
let parameters = deepLinkData.parameters
let source = deepLinkData.source
print("""
✅ 딥링크 수신 완료
- Short Path: \(shortPath)
- Link ID: \(deepLinkData.linkId)
- Source: \(source.description)
- Event Type: \(deepLinkData.eventType)
""")
// UTM 파라미터 추출
let utmParams = deepLinkData.getUtmParameters()
print("📊 UTM Parameters: \(utmParams)")
// 커스텀 파라미터 추출
let customParams = deepLinkData.getCustomParameters()
print("🔧 Custom Parameters: \(customParams)")
// 비즈니스 로직 처리
self.processDeepLink(deepLinkData)
}
}
private func processDeepLink(_ data: DeepLinkData) {
// 파라미터별 분기 처리
if let campaign = data.getParameter("campaign") {
handleCampaignDeepLink(campaign, data: data)
} else if let productId = data.getParameter("product_id") {
handleProductDeepLink(productId, data: data)
} else if let promo = data.getParameter("promo") {
handlePromoDeepLink(promo, data: data)
} else {
handleDefaultDeepLink(data)
}
}
private func handleCampaignDeepLink(_ campaign: String, data: DeepLinkData) {
print("🎯 캠페인 딥링크: \(campaign)")
DispatchQueue.main.async {
// 캠페인 화면으로 이동
NotificationCenter.default.post(
name: .navigateToCampaign,
object: nil,
userInfo: ["campaign": campaign, "data": data]
)
}
}
private func handleProductDeepLink(_ productId: String, data: DeepLinkData) {
print("🛍️ 상품 딥링크: \(productId)")
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .navigateToProduct,
object: nil,
userInfo: ["productId": productId]
)
}
}
private func handlePromoDeepLink(_ promo: String, data: DeepLinkData) {
print("🎁 프로모션 딥링크: \(promo)")
// 프로모션 코드 자동 적용
applyPromoCode(promo)
}
private func handleDefaultDeepLink(_ data: DeepLinkData) {
print("📋 기본 딥링크 처리")
DispatchQueue.main.async {
// 홈 화면으로 이동
NotificationCenter.default.post(name: .navigateToHome, object: nil)
}
}
private func applyPromoCode(_ code: String) {
// 프로모션 적용 로직
print("프로모션 코드 적용: \(code)")
}
}
// Notification 이름 정의
extension Notification.Name {
static let navigateToCampaign = Notification.Name("navigateToCampaign")
static let navigateToProduct = Notification.Name("navigateToProduct")
static let navigateToHome = Notification.Name("navigateToHome")
}2. 디퍼드 딥링크 (자동 처리)
// 디퍼드 딥링크는 AdStageManager.initialize() 시점에 자동으로 처리됩니다.
// 별도 구현 불필요!
// 초기화 시 자동으로:
// 1. 디바이스 핑거프린트 수집
// 2. 서버에 매칭 요청
// 3. 저장된 딥링크가 있으면 자동으로 onDeepLinkReceived 호출
// 필요시 수동 처리:
Task {
await DeferredDeepLinkHandler.shared.handleDeferredDeepLink()
}3. 소스별 처리
func handleDeepLink(_ data: DeepLinkData) {
switch data.source {
case .realtime:
print("🔗 실시간 딥링크 - 앱 실행 중 수신")
// 즉시 화면 전환 가능
navigateImmediately(data)
case .install:
print("📦 디퍼드 딥링크 - 앱 설치 후 첫 실행")
// 온보딩 후 처리 또는 지연 처리
showOnboardingThenNavigate(data)
case .unknown:
print("❓ 알 수 없는 소스")
handleAsDefault(data)
}
}
private func showOnboardingThenNavigate(_ data: DeepLinkData) {
// 온보딩 완료 후 처리
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
self?.navigateImmediately(data)
}
}딥링크 생성
1. Builder 패턴
import AdapterAdStage
class DeepLinkCreator {
// 간단한 딥링크 생성
func createSimpleDeepLink() {
DeepLinkManager.shared.createDeepLink()
.setName("여름 프로모션")
.setDescription("2024년 여름 시즌 프로모션")
.setChannel("google-ads")
.setCampaign("summer2024")
.addParameter(key: "promo", value: "SUMMER20")
.create { [weak self] deepLinkInfo, error in
if let error = error {
print("❌ 딥링크 생성 실패: \(error.localizedDescription)")
return
}
guard let info = deepLinkInfo else {
print("❌ 딥링크 정보 없음")
return
}
print("""
✅ 딥링크 생성 완료
- Short URL: \(info.shortUrl)
- Short Path: \(info.shortPath)
- ID: \(info.id)
""")
// 생성된 URL 공유
self?.shareDeepLink(info.shortUrl)
}
}
// 전체 옵션 포함
func createFullDeepLink() {
DeepLinkManager.shared.createDeepLink()
// 기본 정보
.setName("겨울 프로모션")
.setDescription("2024-2025 겨울 시즌 프로모션")
.setShortPath("WINTER24") // 사용자 지정 경로
// 트래킹 파라미터
.setChannel("facebook-ads")
.setSubChannel("instagram")
.setCampaign("winter2024")
.setAdGroup("fashion-lovers")
.setCreative("banner-001")
.setContent("hero-image")
.setKeyword("winter-sale")
// Android 설정
.setAndroidConfig(
packageName: "com.example.myapp",
appScheme: "myapp://promo/winter",
webUrl: "https://example.com/promo/winter"
)
// iOS 설정
.setIosConfig(
appStoreId: "123456789",
appScheme: "myapp://promo/winter",
webUrl: "https://example.com/promo/winter"
)
// 웹 설정
.setWebConfig(webUrl: "https://example.com/promo/winter")
// 커스텀 파라미터
.addParameter(key: "discount", value: "30")
.addParameter(key: "promoCode", value: "WINTER30")
.addParameter(key: "validUntil", value: "2025-03-31")
.create { deepLinkInfo, error in
if let error = error {
print("❌ 에러: \(error.localizedDescription)")
return
}
guard let info = deepLinkInfo else { return }
print("✅ Short URL: \(info.shortUrl)")
}
}
// 여러 파라미터 한번에 설정
func createDeepLinkWithMultipleParams() {
let parameters: [String: String] = [
"campaign": "spring2024",
"discount": "15",
"promoCode": "SPRING15",
"source": "email"
]
DeepLinkManager.shared.createDeepLink()
.setName("봄 프로모션")
.setChannel("email-marketing")
.setParameters(parameters)
.create { deepLinkInfo, error in
// 처리
}
}
// 공유 기능
private func shareDeepLink(_ url: String) {
DispatchQueue.main.async {
let text = """
🎁 특별 프로모션 초대!
이 링크를 통해 가입하면 5,000원 할인 쿠폰을 드립니다.
\(url)
"""
let activityVC = UIActivityViewController(
activityItems: [text],
applicationActivities: nil
)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let rootVC = windowScene.windows.first?.rootViewController {
rootVC.present(activityVC, animated: true)
}
}
}
}2. async/await 방식
class AsyncDeepLinkCreator {
func createDeepLink() async {
do {
let builder = DeepLinkManager.shared.createDeepLink()
.setName("신규 가입 이벤트")
.setChannel("referral")
.setCampaign("invite-friend")
.addParameter(key: "referrer", value: "USER123")
.addParameter(key: "bonus", value: "5000")
let info = try await builder.createAsync()
print("✅ 딥링크 생성 완료: \(info.shortUrl)")
// UI 업데이트
await MainActor.run {
self.updateUI(with: info.shortUrl)
}
} catch {
print("❌ 딥링크 생성 실패: \(error.localizedDescription)")
await MainActor.run {
self.showError(error)
}
}
}
@MainActor
private func updateUI(with url: String) {
// UI 업데이트
}
@MainActor
private func showError(_ error: Error) {
// 에러 표시
}
}3. Objective-C 호환 방식
// Objective-C에서 사용 가능한 간단한 API
class ObjCCompatibleDeepLinkCreator {
@objc func createSimpleDeepLink(name: String, completion: @escaping (String?, Error?) -> Void) {
AdStageSDK.shared.createSimpleDeepLink(name: name, completion: completion)
}
@objc func createDeepLinkWithDescription(
name: String,
description: String,
completion: @escaping (String?, Error?) -> Void
) {
AdStageSDK.shared.createSimpleDeepLink(
name: name,
description: description,
completion: completion
)
}
}고급 기능
1. 전역 사용자 속성 설정
// 로그인 시
func onUserLogin(userId: String, userProfile: UserProfile) {
AdStageManager.shared.setUserId(userId)
let userAttributes = UserAttributes(
gender: userProfile.gender,
country: userProfile.country,
city: userProfile.city,
age: "\(userProfile.age)",
language: Locale.current.languageCode ?? "en"
)
AdStageManager.shared.setUserAttributes(userAttributes)
print("✅ 사용자 정보 설정 완료")
}
// 로그아웃 시
func onUserLogout() {
AdStageManager.shared.setUserId(nil)
AdStageManager.shared.setUserAttributes(nil)
print("🚪 사용자 정보 초기화")
}2. 전역 디바이스 정보 설정
import UIKit
func setupDeviceInfo() {
let device = UIDevice.current
let app = Bundle.main
let deviceInfo = DeviceInfo(
category: device.userInterfaceIdiom == .pad ? "tablet" : "mobile",
platform: "iOS",
model: device.model,
appVersion: app.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown",
osVersion: device.systemVersion
)
AdStageManager.shared.setDeviceInfo(deviceInfo)
print("""
📱 디바이스 정보 설정:
- Category: \(deviceInfo.category)
- Platform: \(deviceInfo.platform)
- Model: \(deviceInfo.model)
- App Version: \(deviceInfo.appVersion)
- OS Version: \(deviceInfo.osVersion)
""")
}3. 프로모션 배너 연동
import AdapterAdStage
class PromotionManager {
// 프로모션 리스트 조회
func fetchPromotions() async {
let params = AdStageSDK.PromotionParams()
params.bannerType = "NATIVE"
params.region = "KR"
params.limit = NSNumber(value: 10)
if let response = await AdStageSDK.shared.getPromotionList(params: params) {
print("✅ 프로모션 \(response.promotions.count)개 조회")
response.promotions.forEach { promotion in
print("""
- \(promotion.appName)
배너: \(promotion.bannerUrl)
CPI: \(promotion.cpi)
""")
}
}
}
// 프로모션 배너 열기
func openPromotionBanner() {
let params = AdStageSDK.PromotionParams()
params.bannerType = "INTERSTITIAL"
params.region = "KR"
AdStageSDK.shared.openPromotion(
params: params,
showTodayButton: true
) { url, error in
if let error = error {
print("❌ 프로모션 열기 실패: \(error.localizedDescription)")
return
}
if let url = url {
print("✅ 프로모션 URL: \(url)")
}
}
}
}트러블슈팅
1. Universal Link가 작동하지 않음
체크리스트:
- Associated Domains 설정 확인
- apple-app-site-association 파일 배포 확인
- HTTPS 사용 확인
- Team ID와 Bundle ID 일치 확인
검증 방법:
# 1. 파일 접근 가능 여부 확인
curl https://go.myapp.com/.well-known/apple-app-site-association
# 2. Apple CDN 캐시 확인
curl https://app-site-association.cdn-apple.com/a/v1/go.myapp.com디버깅:
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
print("""
Universal Link Debug:
- Activity Type: \(userActivity.activityType)
- Webpage URL: \(userActivity.webpageURL?.absoluteString ?? "nil")
- User Info: \(userActivity.userInfo ?? [:])
""")
return DeepLinkManager.shared.handleUniversalLink(userActivity: userActivity)
}2. URL Scheme이 작동하지 않음
확인 사항:
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
print("""
URL Scheme Debug:
- URL: \(url)
- Scheme: \(url.scheme ?? "nil")
- Host: \(url.host ?? "nil")
- Path: \(url.path)
- Query: \(url.query ?? "nil")
- Source App: \(options[.sourceApplication] ?? "nil")
""")
return DeepLinkManager.shared.handleDeepLink(from: url)
}3. 디퍼드 딥링크가 작동하지 않음
원인:
- 디바이스 핑거프린트 수집 실패
- 서버 매칭 실패
- 네트워크 연결 문제
확인:
// 수동으로 디퍼드 딥링크 처리
Task {
await DeferredDeepLinkHandler.shared.handleDeferredDeepLink()
}
// 로그 확인
print("✅ 디퍼드 딥링크 처리 시작")4. 앱이 백그라운드에 있을 때 딥링크 미수신
SceneDelegate 설정 (iOS 13+):
import UIKit
import AdapterAdStage
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let _ = (scene as? UIWindowScene) else { return }
// URL Context 처리
if let urlContext = connectionOptions.urlContexts.first {
_ = DeepLinkManager.shared.handleDeepLink(from: urlContext.url)
}
// User Activity 처리
if let userActivity = connectionOptions.userActivities.first {
_ = DeepLinkManager.shared.handleUniversalLink(userActivity: userActivity)
}
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let url = URLContexts.first?.url {
_ = DeepLinkManager.shared.handleDeepLink(from: url)
}
}
func scene(
_ scene: UIScene,
continue userActivity: NSUserActivity
) {
_ = DeepLinkManager.shared.handleUniversalLink(userActivity: userActivity)
}
}5. 빌드 설정 확인
Build Settings:
- Deployment Target: iOS 12.0 이상
- Swift Language Version: 5.0 이상
- Enable Bitcode: No (Xcode 14+에서는 제거됨)Info.plist 필수 항목:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>myapp</string>
</array>테스트 방법
1. URL Scheme 테스트
Safari에서 테스트:
myapp://promo/summer
myapp://promo?campaign=summer&discount=20터미널에서 테스트:
xcrun simctl openurl booted "myapp://promo/summer"2. Universal Link 테스트
Safari에서 테스트:
https://go.myapp.com/ABCDEFNotes 앱에서 테스트:
- Notes 앱에서 링크 입력
- 링크를 길게 누르기
- "Open in [앱이름]" 선택
터미널에서 테스트:
xcrun simctl openurl booted "https://go.myapp.com/ABCDEF"3. 로그 확인
// 딥링크 수신 확인
DeepLinkManager.shared.setUnifiedListener { deepLinkData in
print("""
==========================================
딥링크 수신 테스트
==========================================
Short Path: \(deepLinkData.shortPath)
Link ID: \(deepLinkData.linkId)
Source: \(deepLinkData.source.description)
Parameters:
\(deepLinkData.parameters.map { " - \($0.key): \($0.value)" }.joined(separator: "\n"))
==========================================
""")
}
