[iOS] Branch.io 로 딥링크(Deep Link) - Dynamic Link 구현하기

안녕하세요!
25년 8월 25일부터 Firebase의 Dynamic Links 서비스가 종료된다는 소식을 들었습니다(˘̩̩̩ㅅ˘̩ƪ)
Firebase 대신 사용할 수 있는 무료 딥링크(다이나믹 링크) 서비스를 찾던 중, branch.io를 접하게 되었습니다.
MAU가 10K 미만인 서비스는 무료로 딥링크 서비스 사용이 가능하다고 합니다.
0️⃣ 딥링크(Deep Link)와 다이나믹 링크(Dynamic Link)
들어가기 전에, 딥링크와 다이나믹 링크의 차이점부터 짚어보도록 하겠습니다.
딥링크(Deep Link)
앱 내 특정 위치로 이동시키는 URL 링크.
예: myapp://product/123
- 기존 앱 설치자만 사용 가능
- 앱이 설치되어 있어야 동작함
다이나믹 링크(Dynamic Link)
딥링크의 확장 개념으로, 다양한 플랫폼(Android, iOS, 웹)에서 공유 가능하며,
앱이 설치되어 있지 않으면 설치 후에도 원래 목적지로 이동할 수 있게 해주는 스마트 딥링크
- 앱 설치 여부, 플랫폼을 자동으로 감지
- 앱 미설치 시: 앱스토어/플레이스토어로 유도 → 설치 후 원하는 화면으로 이동
- 앱 설치 시: 앱 내 특정 콘텐츠로 바로 이동
- 웹 fallback도 설정 가능
이제 iOS에서 Branch.io 다이나믹링크를 어떻게 세팅하는지 알아보도록 하겠습니다.

1️⃣ Branch.io 세팅
일단 Branch.io 회원가입 진행 후, Dashboard로 진입해주세요. (링크)
[대시보드 > CONFIGURE > App Settings]에서 다음 단계를 따라서 설정해주세요.
참고: https://help.branch.io/docs/basic-link-configuration
1. Required Redirects 설정 (필수)
- Bundle Identifiers는 Xcode Project > Targets > Signing & Capabilities 탭에서 확인 가능.
- AppleAppPrefix는 팀 ID와 동일. Apple Developer 계정에서 확인 가능.
2. Social Media Preview 설정 (선택)
소셜 미디어에 링크를 올렸을 때 생성되는 미리보기를 커스텀할 수 있습니다.
3. Link Domain 변경하기 (권장)
방법: [Change My app.link Subdomain] 버튼 클릭 -> 앱 이름 입력 후 Get 클릭 -> 변경된 도메인 확인
[⚠️ 주의사항]
- 최초 1회만 변경 가능합니다.
- 기존 웹사이트 도메인(main domain) 은 사용하지 마세요.
CNAME 또는 NS 레코드를 변경하면 Branch가 도메인 소유권을 갖게 되며, 기존 웹 콘텐츠에 접근할 수 없습니다.
2️⃣ Xcode 세팅
1. Link Domain 입력하기
1️⃣ 에서 설정한 link 도메인을 Xcode에 입력하는 과정입니다.
1-1. Associated Domains 추가
Xcode Project > Targets > Signing & Capabilities 탭에서
[+ Capability]를 클릭 -> Associated Domains 더블클릭하여 추가해주세요.
만약 "Provisioning profile "staccato-iOS-release" doesn't support the Associated Domains capability." 와 같은 경고가 뜬다면 AppleDevelper > Identifiers에서 Associcated Domains 를 체크한 후, 프로비저닝파일을 갱신하면 됩니다.
1-2. Associated Domains 입력
applinks:subdomain.app.link 의 형태로 도메인을 추가해주세요.
- 도메인은 Branch.io > CONFIGURE > App Settings 의 Link Domain 섹션에서 확인 가능
- Default Link Domain과 Alternate Link Domain 모두 추가
예시) link domain이 myapp.app.link 일 경우 다음 2개 추가
- applinks:myapp.app.link
- applinks:myapp-alternate.app.link
2. Info.plist 세팅하기
Info.plist에 branch_universal_link_domains 키와 branch_key , URL types 키를 추가해야 합니다.
다음 이미지를 참고하여 넣어주세요.
branch_key는 config 파일로 숨겨도 됩니다.
3. Branch 설치하기
3-1. Branch io SDK 설치
Swift Package Manager를 사용하는 게 가장 편하긴 합니다!
"https://github.com/BranchMetrics/ios-branch-sdk-spm"를 검색하여 설치해주세요.
혹시 다른 방법으로 설치하실 분은 이 [링크] 를 참고해주세요.
3-2. dependency 추가
Xcode Project > Targets > Build Phases 탭에서 Link Binary With Libraries 섹션을 펼칩니다.
그리고 아래 이미지에서 Required로 된 라이브러리를 추가해주세요.
3️⃣ 코드 작성: 딥링크 초기화 & 읽기
세팅이 완료되었으니, 이제 코드에서 브랜치를 사용하는 방법을 알아봅시다.
1. branch 초기화
2. 링크를 클릭했을 때 특정 페이지로 이동 (콜드스타트, 웜스타트)
3. 딥링크를 생성
순서로 살펴보겠습니다.
공식문서(링크)에 SwiftUI 프로젝트 세팅 방법은 자세히 잘 나와있으므로,
저는 UIKit 기준으로 설명해보겠습니다.
1. AppDelegate에서 branch 초기화
didFinishLaunchingWithOptions 메소드에 다음 코드를 추가해주세요.
링크를 클릭하면 다음 클로저가 실행됩니다.
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
/// Branch io 초기화
BranchScene.shared().initSession(launchOptions: launchOptions, registerDeepLinkHandler: { (params, error, scene) in
if let params = params as? [String: AnyObject], error == nil {
print("🔗 deepLink: \(params)")
DeepLinkManager.shared.deepLinkParams = params
}
})
return true
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
}
2. DeepLinkManager 생성
import UIKit
import BranchSDK
final class DeepLinkManager {
static let shared = DeepLinkManager()
private init() {}
var deepLinkParams: [String: AnyObject]?
// 딥링크 파라미터에 들어있는 spotId 정보를 가져옵니다.
func getSpotID() -> Int64? {
// NOTE: 딥링크는 한 번만 사용되도록 초기화
defer { deepLinkParams = nil }
// NOTE: iOS 링크
if let spotIDInt64 = deepLinkParams?["spotId"] as? Int64 {
return spotIDInt64
}
// NOTE: Android 링크
else if let spotIDString = deepLinkParams?["spotId"] as? String,
let spotIDInt64 = Int64(spotIDString) {
return spotIDInt64
} else {
return nil
}
}
// 새로 클릭된 딥링크인지 판단합니다.
func isFreshDeepLink() -> Bool {
guard let timeStamp = getClickTimeStamp() else { return false }
let currentTime = Date().timeIntervalSince1970
let timeDelta = currentTime - timeStamp
let isFresh: Bool = timeDelta >= 0 && timeDelta < 20
print("🔗⏱️ stamp: \(timeStamp), current: \(currentTime), delta: \(timeDelta)s, isFresh: \(isFresh)")
return isFresh
}
// 딥링크로 들어왔을 때 spotDetailVC를 present합니다.
func presentSpotDetail() {
if let spotID = getSpotID() {
DispatchQueue.main.async {
if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate,
let window = sceneDelegate.window,
let topVC = window.rootViewController?.getTopViewController() {
let spotDetailVC = SpotDetailViewController(spotID, isDeepLink: true)
spotDetailVC.modalPresentationStyle = .fullScreen
topVC.present(spotDetailVC, animated: true)
}
}
}
}
}
// MARK: - Helper
private extension DeepLinkManager {
func getClickTimeStamp() -> TimeInterval? {
guard let rawValue = deepLinkParams?["+click_timestamp"] else { return nil }
if let timeStamp = rawValue as? TimeInterval {
return timeStamp
} else if let intValue = rawValue as? Int {
return TimeInterval(intValue)
} else if let number = rawValue as? NSNumber {
return number.doubleValue
} else {
print("❌ Invalid timestamp type: \(type(of: rawValue))")
return nil
}
}
}
코드 설명: isFreshDeepLink()
마지막으로 클릭된 딥링크는 캐시에 저장됩니다.
즉, 앱을 종료했다가 딥링크를 통해서가 아닌 직접 앱을 열더라도 해당 캐시 정보때문에 페이지가 이동됩니다.
이를 방지하기 위해, 링크가 클릭된 시간 정보로 새로 클릭된 딥링크인지 여부를 판단합니다.
3. SceneDelegate 설정 (feat. warm start)
import BranchSDK
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: windowScene)
self.window?.rootViewController = SplashViewController()
self.window?.makeKeyAndVisible()
// NOTE:
// Workaround for SceneDelegate `continueUserActivity` not getting called on cold start:
if let userActivity = connectionOptions.userActivities.first {
BranchScene.shared().scene(scene, continue: userActivity)
} else if !connectionOptions.urlContexts.isEmpty {
BranchScene.shared().scene(scene, openURLContexts: connectionOptions.urlContexts)
}
}
func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) {
scene.userActivity = NSUserActivity(activityType: userActivityType)
scene.delegate = self
}
// NOTE: warm start 시 실행됨
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
BranchScene.shared().scene(scene, continue: userActivity)
// NOTE: scene이 딥링크 클로저보다 먼저 불리기때문에 0.5초 지연
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
DeepLinkManager.shared.presentSpotDetail()
}
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
BranchScene.shared().scene(scene, openURLContexts: URLContexts)
}
}
4. SplashVC 설정(선택): cold start 시 페이지 이동
앱의 첫 번째 ViewController에서 다음 코드를 작성합니다.
저의 경우는 Splash ViewController 에서 2초동안 애니메이션이 재생된 후에 화면이 이동되어야하기 때문에
DispatchQueue로 2초 지연시켰습니다.
스플래시를 사용하지 않으신다면 코드를 적당히 수정하시면 될 것 같습니다:D
class SplashViewController: BaseViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(false)
playSplashAnimation()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
if DeepLinkManager.shared.isFreshDeepLink(),
let spotID = DeepLinkManager.shared.getSpotID() {
self.goToSpotDetailVC(with: spotID)
} else {
self.goToNextVC()
}
}
}
// NOTE: 딥링크 진입 시 호출
func goToSpotDetailVC(with spotID: Int64) {
let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
let rootVC: UIViewController = ACTabBarController()
sceneDelegate?.window?.rootViewController = rootVC
sceneDelegate?.window?.makeKeyAndVisible()
let spotDetailVC = SpotDetailViewController(spotID, isDeepLink: true)
spotDetailVC.modalPresentationStyle = .fullScreen
rootVC.present(spotDetailVC, animated: true)
}
}
4️⃣ 코드 작성: 딥링크 생성하기
딥링크를 만들어야 하는 버튼 (ex.공유버튼)의 액션 클로저에 다음 코드를 추가합니다.
그러면 딥링크 정보를 담은 ShareSheet을 present할 수 있습니다.
참고: 저는 shareButton이 UIButton이 아니라서 addTarget 메소드 대신 클로저를 사용했습니다.
// SpotDetailViewController
spotDetailView.shareButton.onTap = { [weak self] _ in
self?.viewModel.createBranchDeepLink() { [weak self] context in
guard let self = self else { return }
let activityVC = UIActivityViewController(activityItems: [context], applicationActivities: nil)
self.present(activityVC, animated: true, completion: nil)
AmplitudeManager.shared.trackEventWithProperties(AmplitudeLiterals.EventName.detailPage, properties: ["click_share?": true])
}
}
다음은 딥링크를 생성하는 코드입니다.
customMetadata에 dictionary 형식으로 원하는 정보를 마음껏 추가하면 됩니다.
그러면 딥링크 클릭 시 AppDelegate에서 작성했던 딥링크 클로저의 params에 이 메타데이터가 포함될 것입니다.
// viewModel
func createBranchDeepLink(_ completion: @escaping (String) -> ()) {
guard let buo: BranchUniversalObject = makeBranchUniversalObject() else { return }
let lp: BranchLinkProperties = makeBranchLinkProperties()
buo.getShortUrl(with: lp) { [weak self] url, error in
if let error {
print("🔗❌ 딥링크 생성 실패: \(error.localizedDescription)")
return
}
guard let url = url else {
print("🔗❓ 딥링크 생성은 성공했으나 URL == nil")
return
}
let spotName = self?.spotDetail?.name ?? ""
let description = StringLiterals.DeepLink.atAcon + spotName + StringLiterals.DeepLink.checkOut
let context: String = description + "\n" + url
completion(context)
print("🔗✅ deeplink 생성 성공: \(url)")
}
}
private func makeBranchUniversalObject() -> BranchUniversalObject? {
guard let spot = spotDetail else { return nil }
let buo: BranchUniversalObject = BranchUniversalObject(canonicalIdentifier: "spot/\(spot.spotID)")
buo.title = StringLiterals.DeepLink.deepLinkTitleAcon + " " + spot.name
buo.contentDescription = StringLiterals.DeepLink.deepLinkDescription
buo.contentMetadata.customMetadata["spotId"] = spot.spotID
return buo
}
private func makeBranchLinkProperties() -> BranchLinkProperties {
let lp: BranchLinkProperties = BranchLinkProperties()
lp.channel = StringLiterals.DeepLink.branchLinkChannel
lp.feature = StringLiterals.DeepLink.branchLinkFeature
lp.addControlParam(StringLiterals.DeepLink.branchDeepLinkPathParamName, withValue: "spot/\(spotID)")
return lp
}
📷 결과
1. 앱이 없을 경우: AppStore / PlayStore로 이동
2. 앱이 있을 경우 (ColdStart, WarmStart)
3. 공유버튼 클릭 시
📘 branch.io setting Document
- https://help.branch.io/developers-hub/docs/ios-basic-integration
- https://help.branch.io/developers-hub/docs/ios-advanced-features

감사합니다!