[iOS] URL Scheme를 통한 딥링크 설계 및 구현
📚 딥링크란?
딥링크란 모바일 앱의 특정화면이나 콘텐츠로 직접 이동할 수 있도록 해주는 링크이다.
예를 들면 카카오톡에서 채팅이 왔을 때 푸시알람을 통해 특정 채팅방으로 바로 이동할 수 있도록 하는 것이나 특정 푸시알람을 클릭했을 때 해당 앱으로 접속하도록 하는 동작이다.
딥링크를 구현하는 방법은 보통 FireBase의 Dynamic Links, URL Scheme, Universal Link 정도가 있는데 파이어베이스에서 제공하는 Dynamic Links는 지원이 중단 됐으므로 URL Scheme와 Universal Links에 대해서 알아보겠다.
URL Scheme
URL Scheme는 Xcode에 URL types를 등록하여 사용하며 가장 기본적인 딥링크 방식이다. 이 기능을 사용하려면 앱이 설치되어 있어야 사용 가능하며, 앱이 설치되지 않은 경우는 사용하지 못한다.
iOS 9이전 버전까지 사용할 수 있었던 것이 URL Scheme이며, 9 이상 나온 게 Universal Link이기 때문에 버전 제약이 없다는 것이 장점이다. 또한 커스텀 파라미터를 전달하기 편하기 때문에 구현하기 간단하다는 장점이 있다.
(하지만 블로그나 참고자료가 많이 없어서 구현하기 쉽다고 느껴지지는 않았다.)
URL Scheme를 사용하기 위한 URL Types가 필요하며 공식문서에서는 아래와 같은 url 타입을 예시로 들고 있다.
myphotoapp:albumname?name="albumname"
myphotoapp:albumname?index=1
Universal Link
Universal Link는 웹사이트의 도메인을 등록하여 앱과 웹사이의 양방향 연결이 가능하도록 도와준다. 즉 웹서버가 필요하다.
Universal Link의 장점은 앱이 설치되어 있지 않아도 사용 가능하지만 웹 서버가 존재하지 않다면 사용할 수 없다!!
즉 웹서버를 사용하지 않는다면 이 딥링크 기능만을 위해 웹서버를 띄워야 하므로 추가비용이 발생하게 된다는 것이다.
앱이 설치되어 있지 않다면 딥링크를 눌렀을때 앱을 설치하도록 앱스토어로 넘어가는 방식으로 주로 사용하기도 하며, 보안성이 높은 것이 장점이다.
하지만, 지금 구현하고자 하는 경우는 채팅항목에 대한 실시간 푸시알람을 통해 특정 채팅방으로 이동하는 것이므로 앱을 사용하는 사용자들끼리의 목적이기 때문에 URL Scheme 방식을 채택하였다.
또한 URL Scheme에서 앱스키마를 설정해주고, 채팅방 ID만 넘겨받아서 파라미터에 넣어주면 되었기 때문에 현재 주어진 시간 안에 빠르게 개발할 수 있는 것은 URL Scheme이라고 판단하였다.
✏️ 딥링크 설계
초기설계
위 다이어그램은 내가 그린 다이어그램을 바탕으로 팀원이 더 자세한 플로우로 작성해준 다이어그램이다.
하지만 위 다이어그램을 바탕으로 설계하지 않은 이유는 뷰 처리 방식에서의 문제 때문이였다.
현재 프로젝트에서는 MVVM + 클린 아키텍처 코드로 구현 중이었으며 채팅 쪽 뷰는 모두 의존성 주입과 Factory Pattern을 사용하여 화면 전환을 구현하고 있었기에, 위 설계를 바탕으로 구현할 경우 화면 이동 과정에 지나치게 복잡해졌다.
현재 채팅 화면으로의 이동경로는 다음과 같다.
루트뷰 -> 메인 탭뷰 -> 채팅 셀 뷰 -> 채팅방 이렇게 화면이동을 거쳐야 하는데, 루트뷰에서 채팅방까지의 의존성 주입을 관리하는 것이 매우 까다로웠다. 채팅방 진입 시점에서 필요한 viewModel과 의존성을 루트뷰에서부터 단계적으로 전달해야 하는데 이 과정이 너무 복잡해지면서 오히려 흐름파악이 제대로 되지 않았다.
따라서 화면 전환을 단순화하고 딥링크 기능에 조금 더 충실하도록 설계 방향을 수정하였다.
최종 설계
두 번째 설계는 최대한 플로우를 간단하게 작성하려고 하였다.
구현은 크게 세 가지 부분으로 나뉜다.
- 전역 상태 관리를 위한 ChatNavigationState
- DeepLink 처리를 위한 DeepLinkCoordinator
- 상태 변화를 감지하고 화면을 전환하는 MainTabView
ChatNavigationState이라는 싱글톤 클래스를 추가하여 채팅방 이동에 필요한 상태를 전역적으로 관리함으로써 동일한 상태를 공유할 수 있도록 하였으며, 이후 DeepLinkCoordinator가 ChatNavigationState를 사용하도록 하여 채팅방 이동시 ChatNavigationState를 통해 상태를 업데이트하도록 하여 뷰를 변경해 주는 것이다!
화면이동은 루트뷰인 MainTabView에서 ChatNavigationState 상태를 구독하고 있다가 ChatNavigationState의 상태가 변경되면 자동으로 채팅탭으로 전환되도록 한다. 그 이후에는 이동한 채팅셀에서 chatRoomId를 받아 해당 채팅방으로 이동하도록 한다.
즉 정리하면 전체적인 흐름은 아래와 같다.
1. AppDelegate에서 푸시알람을 수신 -> DeepLinkURL을 생성
2. DeepLinkCoordinator는 넘겨받은 url을 채팅방 id 정보만 추출 & 현재 상태를 ChatNavigationState에 업로드
3. ChatNavigationState에서 변경을 감지
4. 상위뷰인 MainTabView에서 onChange로 navigationState.shouldNavigateToChatRoom의상태가 변경되었을 경우 채팅 탭으로 이동
5. ChatCellView에서 마찬가지로 onChange로 navigationState.shouldNavigateToChatRoom의 상태를 변경됨을 감지하면 딥링크를 통해 받은 ChatRoomId를 넘겨줘서 해당 ChatRoomView로 이동
1️⃣ URL Scheme 등록
먼저 URL Scheme를 사용하기 위해선 scheme를 등록해줘야 한다.
특정 URL Scheme로 들어오는 요청을 어떤 뷰로 라우팅 할지 알아야 하기 때문에 예를 들어 myphotoapp:albumname?index=1 에서 myphotoapp에 해당하는 앱 스키마를 등록하기 위한 절차이다.
Project -> Target -> Info -> URL Types
2️⃣ 구현
DeepLinkCoordinator
final class DeepLinkCoordinator: ObservableObject {
@Published var currentChatRoomId: Int64? = nil
private let navigationState = ChatNavigationState.shared
func handleDeepLink(url: URL) {
guard url.scheme == "pennyway",
url.host == "chat",
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItem = components.queryItems?.first(where: { $0.name == "roomId" }),
let roomId = queryItem.value,
let chatRoomId = Int64(roomId)
else {
Log.fault("⚠️ Failed to parse deep link URL: \(url)")
return
}
Log.info("✅ DeepLink detected: Moving to chatRoomId: \(chatRoomId)")
DispatchQueue.main.async {
self.currentChatRoomId = chatRoomId
self.navigateToChatRoom(chatRoomId: chatRoomId)
}
}
private func navigateToChatRoom(chatRoomId: Int64) {
DispatchQueue.main.async {
self.currentChatRoomId = chatRoomId
self.navigationState.navigateToChatRoom(id: chatRoomId)
Log.info("[DeepLinkCoordinator]:🌐 Navigate to ChatRoom with ID: \(chatRoomId)")
}
}
}
DeepLinkCoordinator는 넘겨받은 url을 채팅방 id 정보만 추출하고 현재 상태를 ChatNavigationState에 업로드하도록 한다.
그러기 위해선 handleDeepLink에서 url을 받아 특정 채팅방으로 이동할 수 있는지 확인한 후 이동할 수 있도록 url을 URLComponents로 변환하여 쿼리파라미터를 추출하여 chatRoomId 값을 찾아서 currentChatRoomId에 값을 업데이트하고 있다.
또한 채팅방 이동을 위해 navigateToChatRoom에서 넘겨받은 채팅방 id를 현재 채팅방 ID로 업데이트 한 다음 ChatNavigationState의 navigateToChatRoom을 호출하여 특정 채팅방으로 화면이동이 가능하도록 구현하였다.
ChatNavigationState
import Foundation
import SwiftUI
// MARK: - ChatNavigationState
final class ChatNavigationState: ObservableObject {
@Published var selectedChatRoomId: Int64?
@Published var shouldNavigateToChatRoom: Bool = false
static let shared = ChatNavigationState()
func navigateToChatRoom(id: Int64) {
DispatchQueue.main.async {
self.selectedChatRoomId = id
self.shouldNavigateToChatRoom = true
}
}
func resetNavigation() {
selectedChatRoomId = nil
shouldNavigateToChatRoom = false
}
}
ChatNavigationState는 채팅방 이동에 필요한 상태를 전역적으로 관리할 싱글톤 클래스로 구현하였다. 따라서 현재 선택된 채팅방 ID를 저장하고 채팅탭으로의 이동 여부 상태를 관리한다.
.onChange(of: navigationState.shouldNavigateToChatRoom) { showNavigate in
if showNavigate {
// 딥링크를 통해 채팅 탭으로 이동
viewModel.selection = 2
Log.debug("[MainTabView]: \(String(describing: navigationState.selectedChatRoomId))")
DispatchQueue.main.async {
navigationState.shouldNavigateToChatRoom = false
}
}
}
따라서 MainTabView에서 ChatNavigationState.shared.shouldNavigateToChatRoom의 상태가 변경되었다면, 채팅탭으로 이동하였고, 채팅탭에서도 동일하게 onChange를 통해 ChatNavigationState가 변경됨을 감지하여 특정 채팅방으로 이동하도록 구현하였다.
🚨 주의할 점
MainTabView -> 채팅 탭 -> 특정 채팅방으로 이동하는 과정에서 모두 onChange를 통해 감지하도록 구현을 했는데, 채팅탭에서 onChange로 ChatNavigationState.shared.shouldNavigateToChatRoom를 감지하였다면, 이후 채팅방에서 onChange를 통해 동일한 ChatNavigationState 상태를 감지하면 onChange가 작동되지 않는 문제가 발생한다.
왜냐면 onChange는 값이 변경될 때만 실행되는데, MainTabView -> 채팅탭으로 이동할 때 이미 상태를 감지하였으니 채팅 탭->특정 채팅방으로 이동할 땐 상태가 변경되지 않았다고 가정하기 때문에 onChange가 감지되지 않아서 특정 채팅방으로 이동하지 못했었다.
따라서 MainTabView -> 채팅탭으로 onChange를 통해 상태를 감지하였으면 shouldNavigateToChatRoom의 상태를 반드시 초기화시켜주어야 다음 뷰에서도 onChange를 통해 감지할 수 있게 되어 채팅방 내부 뷰까지 진입할 수 있는 것이다.
만약 화면이동을 이렇게 단계적으로 하지 않고 루트뷰 -> 특정 뷰 까지 진입이 가능하다면 위의 고려사항은 딱히 신경 쓸 필요가 없긴 하다.
3️⃣ 테스트
위에 시뮬레이터를 통해 푸시알람을 누르면 해당 채팅방으로 자동으로 이동하게 된다
물론 앱 실행중일 때도 가능하고 앱이 종료되고 나서도 푸시알람을 클릭했을 때 자동으로 이동된다!!
위에 구현한 딥링크를 바탕으로 관련 코드를 참고하고 싶으면 아래 PR과 코드를 참고하면 좋을 거 같다!
💻 딥링크 구현 by yanni13 · Pull Request #309 · CollaBu/pennyway-client-ios
작업 이유 딥링크 구현 작업 사항 1️⃣ 딥링크 구현 URL Scheme를 통해 딥링크를 구현하였으며 대략적인 플로우는 아래와 같다. 1. AppDelegate에서 푸시알람을 수신하여 DeepLinkURL을 생성 2. DeepLinkCoordi
github.com