일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- @available
- 독후감
- 야곰아카데미
- contentInset
- Codegen
- 부트캠프
- roundingMode
- 스위프트
- 책후기
- delegation
- xcode
- Modality
- IOS
- Structures and Classes
- View Life Cycle
- NotificationCenter
- 독서후기
- 스타트업주니어로살아남기
- Info.plist
- viewcontroller
- mvvm
- human interface guidelines
- Failed to register bundle identifier
- SWIFT
- 아이폰
- NumberFormatter
- SWIFTUI
- Mock
- UIResponder
- Navigation
- Today
- Total
호댕의 iOS 개발
[iOS] Universal Links에 대해 알아보자 본문
이전 푸쉬 알림을 진행하며 원링크의 푸쉬 알림에 유니버셜 링크를 전달받아 이를 통해 앱의 특정 컨텐츠를 여는 작업을 했었다.
푸쉬 알림을 받을 때의 처리는 직접 했지만 유니버셜 링크에 대한 처리는 직접 하진 못했었다.
그래서 UniversalLinks에 대해 공부해보고자 마음먹었고, WWDC 2019 What's New in Universal Links를 보며 Universal Links에 대해 정리해보고자 한다.
(아래 글은 WWDC 2019 What's New in Universal Links를 보고 정리한 글입니다)
+ 2019년, 2020년 모두 What's New in Universal Links라는 이름의 WWDC 영상이 있는데, 2019년 영상이 좀 더 제너럴한 유니버셜 링크에 대한 설명이고 2020년은 WatchOS에 중점을 두고 설명을 하고 있어 2019년 영상 위주로 보고 작성했습니다.
Universal Links는 뭘까?
Universal Links는 iOS 9부터 도입되어 웹과 앱 모두 풍부한 컨텐츠를 제공하기 위해 등장했다. 앱과 웹 간에 Universal Links를 통해 안전한 연결을 지원하는 것이다.
다시 정리해보면 이는 Apple의 운영체제가 웹 또는 앱의 리소스를 인식할 수 있도록 하는 HTTP 또는 HTTPS URL이다. 즉, 사용자가 앱을 설치했든 아직 다운로드를 하지 않았든에 관계 없이 단일 URL을 통해 지정된 컨텐츠를 보여줄 수 있도록 하는 것이다.
이를 통해 사용자의 앱 참여도 또한 높일 수 있다.
이는 UIKit 뿐만 아니라 AppKit을 사용하더라도 사용이 가능하다.
이를 지원하기 위해선 앱에선 Xcode에선 도메인을 나타내는 identifier를 채택해줘야 하고, 웹의 경우 앱에서 표현하는 도메인에 대한 자세한 내용이 포함된 단일 JSON 파일을 채택해야 한다.
이런 웹과 앱 간의 보안 Handshake를 통해 앱을 구성한 개발자를 제외한 누구도 사용자를 임의로 앱으로 리디렉션을 할 수 없게 된다.
딥링크를 통해 Custom URL Scheme을 사용하는 경우 본질적으로 안전하지 않으며, 악의적인 개발자가 이에 대한 리디렉션을 남용할 수도 있다.
Universal Links 구축하기
Web Server 구축하기
일단 Universal Links를 구성하기 위해선 유효한 HTTPS 인증서가 있어야 한다.
HTTP의 경우 안전하지 않으며, 웹과 앱을 연결하는데 사용할 수 없다. 또한 HTTPS 인증서에 서명하는데 사용되는 Root Certificate의 경우 Apple의 운영체제가 인식할 수 있어야 한다.
(이 때 사용자 지정 Root Certificate는 지원하지 않는다 -> 2019년 이후로 apple-app-site-association에 대한 서명은 필요하지 않는다고 한다)
인증서를 생성하고 서버를 구성한 후 apple-app-site-association 파일을 추가해줘야 한다. 이때 apple-app-site-association 파일은 JSON 형식으로 되어 있어야 한다.
앱이 Apple 디바이스에 설치되면 운영체제는 apple-app-site-association 파일을 다운로드해서, 서버에서 앱을 사용할 수 있도록 허용하는 서비스를 결정하게 된다. (다양한 서비스 중 하나가 바로 Universal Links이다)
처음 앱을 설치할 때만 다운로드하는 것이 아니라, 시스템에서 해당 파일에 대한 업데이트를 주기적으로 다운로드하게 된다.
apple-app-site-association 파일은 반드시 HTTPS://{도메인 이름}/.well-known/apple-app-site-association에 있어야 한다. 다른 경로는 허용되지 않는다.
apple-app-site-association 파일 구성
apple-app-site-association 파일 중에서 Universal Links에 해당하는 부분에 대해 살펴보자.
// apple-app-site-association 파일
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABCDE12345.com.example.app",
"paths": [ "/path/*/filename" ] // iOS 13 이전
"components": [ // iOS 13 이후 paths 키 대체
"/": "/path/*/filename",
"#": "*fragment",
"?": "widget=?*"
// "?": { "widget": "?*", "grommet": "please" }
"exclued": true // 만약 아직 앱에서 표시하지 않을 웹 내용인 경우 해당 Key를 true로 주면 됨
]
}
]
}
}
전체적으로 딕셔너리 형태로 구성이 되어 있다.
applinks
Universal Links의 경우 applinks 가 핵심이다.
하위에는 apps 와 details 키로 구성되어 있다.
apps
apps 키는 iOS 13부터는 해당 키가 필요하지 않기 때문에 생략해도 괜찮다. 이전 버전을 지원하는 경우에는 해당 키를 써줘야 하며 Universal Links를 사용할 때에는 해당 키는 항상 빈 배열을 줘야 한다.
details
details 키는 하위에 앱 식별을 위한 appID 키를 가지고 있다. details 키는 Dictionary 배열 형태로 되어 있으며 특정 앱의 Universal links 구성을 나타낸다.
appID
앱을 식별하기 위한 appID 의 value는 Apple에서 제공한 영숫자 10자리를 접두사로 가지고 뒤에 마침표를 찍은 뒤 Bundle Identifier를 붙이게 된다.
앞에 붙는 영숫자의 경우 Team ID일 수도 있고 아닐 수도 있다. (보통 Team ID를 사용하는 것 같다)
Team ID는 Apple Developer에 로그인한 후 멤버십 세부 사항을 보면 알 수 있다.
만약 동일한 Universal links 구성을 가진 여러 앱이 있는 경우 appIDs 키를 사용해서 배열 형태로 작성할 수도 있다.
// 만약 동일한 Universal link를 사용하는 앱이 여러 개인 경우 "appID" 대신 이렇게도 작성 가능
"appIDs": [ "ABCDE12345.com.example.app", "ABCDE12345.com.example.app2" ]
paths
paths 키의 경우 터미널에서 사용하는 것과 동일하게 패턴 매칭을 하게 된다.
- * : 여러 와일드 카드 문자를 나타내는데 사용한다. (빈 문자열을 포함한 모든 문자열과 일치)
- ? : 하나의 문자를 나타내는데 사용된다.
components
components 키의 경우 iOS 13부터 paths 키를 대체하는 키이다.
이는 딕셔너리 배열로 구성되어 있으며, 키가 #인 URL의 구성요소와도 일치할 수 있고, 키가 ?인 쿼리 구성 요소와도 일치시킬 수 있다.
쿼리 구성요소의 경우 문자열 대신 딕셔너리 형태로 Value를 지정할 수 있으며, 개별 쿼리 항목을 작성할 때와 동일한 방법으로 작성해주면 된다.
URL에 #이 들어가는 경우는 아직 보지 못했었는데, 브라우저가 리로딩 없이 자바스크립트를 불러오기위한 방법이라고 한다.
// 패턴 매치 예시
"appIDs": [ "ABCDE12345.com.example.app" ],
"components": [
{
"/": "/*/order/" // https://example.com/{*}/order/
},
{
"/": "/taco/*",
"?": { "cheese": "?*" } // https://example.com/taco/cheese=panela
},
{
"#": "coupon-1???", // https://example.com#coupon-1234
"exclude": true
}
]
전 세계 사용자들을 위한 지원
사실 이 부분은 아직 잘 와닿지는 않았지만 정리를 해본다.
내가 이해한 부분은 패턴 매칭이 ASCII로 수행되며 apple-app-site-association를 다운받을 때 우선순위가 정해져서 받아진다는 것이다.
URL의 경우 항상 ASCII로 코딩이 되기 때문에, 패턴 매칭도 ASCII로 수행된다.
만약 국가 별로 패턴 매칭을 달리 제공하고 싶은 경우, JSON의 크기가 커질 수 있다. 따라서 국가별 패턴 매칭을 통일한다면 JSON 파일의 크기를 줄일 수 있다.
iOS 13부터 운영체제가 사용자가 탐색할 가능성이 높은 지역을 기반으로 apple-app-site-association의 다운로드 우선순위를 지정하게 된다. 물론 앱이 설치될 때 우선순위를 정하는 것이지 모두 다운을 하긴 한다.
- 최상위 도메인인 .com, .net, .org의 경우 전 세계적으로 많은 트래픽을 차지하기 때문에 우선순위가 높은 도메인이다.
- ccTLD 및 국제화된 TLD라고도 하는 국가 코드 TLD의 경우도 사용자의 locale 설정과 일치하는 경우 높은 우선순위로 지정된다.
여기서 TLD는 도메인 네임의 가장 마지막 부분을 의미한다. 즉, .com, .net, .org 등이 TLD로 최상위 도메인인 것이다.
국가 코드 TLD는 다음 링크와 같다.
App 구성하기
가장 많이 작업하게 될 부분이다.
일단 Universal links를 적용하기 위해선 Xcode의 프로젝트 설정을 추가해줘야 한다.
프로젝트 Targets에서 Signing & Capabilities에서 빨간색으로 표시된 버튼을 눌러 Associated Domains를 추가해주면 된다.
여기서 도메인 형식의 문자열 배열을 작성해주면 된다.
<array>
<string>applinks:www.example.com</string>
<string>applinks:*.example.com</string> // 이 경우 example.com을 방문하게 된다.
// 정확한 도메인의 경우 와일드 카드 도메인보다 Universal Link 조회에서 더 높은 우선 순위를 갖는다
</array>
이런 식으로 applinks:를 앞에 붙여준 후 도메인 이름을 String 형태로 작성해주면 된다.
배열의 Element 순서는 시스템에서 무시된다. 즉, 순서는 크게 중요하지 않다.
위처럼 앱에 도메인까지 설정해주게 되면 다음 순서대로 처리가 된다.
1. www.example.com에 대해 Universal Links를 지원함을 선언하게 된다.
2. 앱이 설치되면 www.example.com에 방문해 apple-app-site-association 파일을 찾게 된다.
3. apple-app-site-association 파일이 존재하고 앱에 대한 정보, 앱 식별자가 제대로 포함되어 있다면 연결을 확인한다.
국제화된 도메인의 경우 Punycode(RFC 3492에 정의됨)를 통해 인코딩을 해줘야 한다고 한다.
Punycode의 경우 변환을 하게 되면 "xn--"으로 시작되며, 도메인 이름에 쓸 수 있는 문자만으로 표기하기 위해 만들어진 인코딩 방식이라고 한다.
이렇게 프로젝트 구성도 완료되었다면 Universal Links로 들어왔을 때에 대한 처리를 해줘야 한다.
이는 AppDelegate의 application(application:userActivity:restorationHandler:) -> Bool 메서드를 사용해 처리해줄 수 있다.
여기서 true가 반환되면 활동을 정상적으로 처리했음을 나타내는 것이고 false를 반환하면 제대로 처리되지 않았음을 알리게 된다.
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool
{
// Get URL components from the incoming user activity.
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, // WWDC에선 이 부분을 검증하는 것을 권장한다
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
return false
}
// Check for specific URL components that you need.
guard let path = components.path,
let params = components.queryItems else {
return false
}
print("path = \(path)")
if let albumName = params.first(where: { $0.name == "albumname" } )?.value,
let photoIndex = params.first(where: { $0.name == "index" })?.value {
print("album = \(albumName)")
print("photoIndex = \(photoIndex)")
return true
} else {
print("Either album name or photo index missing")
return false
}
}
만약 SceneDelegate를 사용하는 경우 scene(_:willConnectTo:options:) 메서드를 사용할 수도 있다.
func scene(_ scene: UIScene, willConnectTo
session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// Get URL components from the incoming user activity.
guard let userActivity = connectionOptions.userActivities.first,
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
return
}
// Check for specific URL components that you need.
guard let path = components.path,
let params = components.queryItems else {
return
}
print("path = \(path)")
if let albumName = params.first(where: { $0.name == "albumname" })?.value,
let photoIndex = params.first(where: { $0.name == "index" })?.value {
print("album = \(albumName)")
print("photoIndex = \(photoIndex)")
} else {
print("Either album name or photo index missing")
}
}
다른 응용 프로그램에서 Universal Links를 열고 싶을 때에는 아래 함수를 사용하면 된다.
ex) 내 앱에서 카카오톡 같은 다른 앱을 열고 싶은 경우 url에 카카오톡의 Universal links를 넣으면 된다.
UIApplication.shared.open(url, options: [ .universalLinksOnly: true ]) {
•••
}
Universal Links를 사용하는 Best Practices
우아하게 실패를 처리하자
만약 Universal Links를 통해 여는 것을 실패했더라도 Safari에서 해당 URL을 여는 것을 고려하거나, 최소한 문제에 대한 세부 정보를 보여주는 메세지 정도는 띄워주자.
사용자를 빈 화면으로 보내지 말자
Universal Links를 통해 들어갔을 때 사용자가 아무 것도 없는 화면을 보지 않도록 하자.
차라리 Safari로 연결시켜 스마트 앱 배너를 띄울 수 있도록 하자.
해당 WWDC 영상을 보며 처음으로 Smart App Banner에 대해 들어 이에 대해 좀 더 찾아봤다.
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
Smart App Banner
Smart App Banner의 경우 다른 판촉 방법에 비해 사용자 경험을 크게 향상시키게 된다. 이는 일관된 모양과 느낌을 제공해서 배너를 탭하는 경우 타사 광고처럼 동작하는 것이 아니라 App Store로 연결된다고 예상할 수 있는 것이다. 또한 사용자 대부분 전체 화면으로 뜨는 배너보다 웹 페이지 상단에 위치해 상대적으로 눈에 잘 띄지 않는 배너를 더 선호한다고 한다.
또한 X 버튼을 눌러 쉽게 배너를 닫을 수 있고, 이 경우 다시 해당 웹 사이트를 들어가더라도 배너가 다시 나타나지 않는다.
이미 앱이 설치된 경우에는 App Store로 이동하지 않고 설치된 앱으로 이동하게 된다.
만약 Smart App Banner를 추가하려고 한다면, 웹사이트 각 페이지의 head에 다음 메타 태그를 넣어주면 된다.
<meta name="apple-itunes-app" content="app-id={내 앱스토어 앱 ID}, app-argument={ 앱의 Universal Links를 넣으면 되는 것 같다 }">
content에서 app-id의 경우 필수로 작성해야 하며, app-argument의 경우 optional이다.
참고 문서
- https://developer.apple.com/videos/play/wwdc2019/717/
- https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623072-application
- https://developer.apple.com/documentation/xcode/supporting-associated-domains
- https://developer.apple.com/documentation/uikit/uiscenedelegate/3197914-scene
- https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
'Software Engineering > iOS' 카테고리의 다른 글
[RxSwift] 버튼을 누르는 이벤트를 일정 시간 동안 한 번만 보내고 싶다면 (Throttle, Debounce) (0) | 2022.11.07 |
---|---|
[iOS] AB 테스트를 위한 툴, Hackle (0) | 2022.11.07 |
[iOS] 인스타그램 스토리 공유하기 (+ UIGraphicsImageRenderer) (0) | 2022.10.25 |
[iOS] 푸쉬 알림(Push Notification) 어떻게 할까? (OneSignal, FCM, Foreground에서 알림 받지 않기 등등) (0) | 2022.09.29 |
[Xcode] Developer Mode(개발자 모드)가 필요합니다...? (0) | 2022.09.19 |