본문 바로가기

학습 노트/iOS (2021)

138 ~ 139. Remote Notification & Push Certificates, Provider and Client App

Remote Notification & Push Certificates

Remote Notification의 구현 방식은 이전의 Local Notification과 동일하다고 봐도 무방하다.
실제로 사용자에게 비춰지는 Remote Notification과 Local Notification은 시각적으로도,
기능적으로도 동일하기 때문에 이를 구별하기 힘들다.

하지만 Remote Notification은 원격서버에서 전송하는 Notification이기때문에 차이점도 존재한다.
Remote Notification을 실제로 전달하는 서버는 애플의 서버로 APNs라고 부른다.
이 APNs로 Notificatio을 전달하는 서버를 Provider라고 부르고 이는 직접 구현하거나 클라우드 서비스를 사용해야한다.
강의에서는 Azure를 통해 Provider를 구현한다.

Local Notification을 예약할 때는 UNMutableNotificationContent 클래스의 인스턴스를 생성해야한다.
이후 인스턴스에 필요한 정보를 구성한 다음 User Notification Center로 전달한다.
User Notification Center가 예약된 시점에 Notification을 전달한다.

Remote Notification을 예약할 때는 Provider에서 JSON으로 Notification을 구성하고 이를 APNs로 전달한다.
이 때 JSON으로 구성한 정보를 Payload라고 부른다.
APNs는 Payload가 포함된 요청을 받으면 기기로 Notification을 전달한다.
Network를 통해 전달되기 때문에 Local Notification과 달리 지연되거나 전달되지 않을수도 있다.

즉, 기기가 꺼져있는 경우에도 전달되지 않는다.
APNs는 기기의 상태를 확인하고 기기가 꺼져있거나 Network에 연결되어있지 않다면
대기목록에 저장했다가 시차를 두고 다시 전달한다.
대기목록에 이미 저장된 항목이 있다면 가장 최근 항목으로 대체하고 이전의 항목을 삭제한다.
또한, 대기시간이 길어지면 모든 대기목록을 삭제하기도 한다.
따라서 이를 감안하여 구현해야한다.

Local Notification은 사용자에게 권한을 획득 하는 것으로 초기화가 완료된다.
하지만 Remote Notification은 몇가지 작업이 더 필요하다.
Provisioning Portal에서 App ID를 만들고(Configuring Remote Notification Support), 인증서를 생성한 다음,
Project에서 RemoteNotification이 사용되도록 설정해야한다.
이후 사용자에게 권한을 얻어 APNs에 기기를 등록하고(Registering to Receive Remote Notifications),
Token을 발급받아 이를 Provider에 전송해야한다.(Obtaining a Device Token)
Token은 APNs가 기기를 구별하는 용도로 사용한다.

Remote Notification 구현은 크게 세가지로 단계로 구분할 수 있다.

  • Generating APNS Certificates
    개발용 Mac에서 인증서 서명 요청 파일을 만들고, iOS Provisioning Portal에서 Push 인증서를 생성한다.
    이후, Download한 인증서를 KeyChain에 저장한다.
  • Setting Up Provider
    Azure가 제공하는 Notification Hub를 만들고, 앞서 만든 인증서를 사용해 APNs와 통신할 수 있도록 구성한다.
  • Implementing Client App
    MS가 제공하는 Framework를 통해 Provider에 Device를 등록하고 Push를 받을 수 있도록 구현한다.

인증서 서명 요청 파일을 만들기 위해 KeyChain을 실행한다.

Keychain 메뉴의 Certificate Assistant를 선택하고 Request a Certificate From a Certificate Authority를 선택한다.

Email 주소와 사용자 명을 기입하고, Saved to disk를 선택해 Mac에 저장한다.

이번엔 Apple Developer 사이트에서 Push 인증서를 생성해야한다.

 

Apple Developer

There’s never been a better time to develop for Apple platforms.

developer.apple.com

개발자 페이지에서 로그인을 한 뒤 콘솔로 진입한다.
왼족의 Certificate, Identifier & Profiles를 선택한다.

이후 사이드바에서 Identifiers를 선택하고,'+' 버튼을 눌러 Identifier를 추가한다.

그 다음에는 App IDs를 선택하고, App을 선택해 넘어간다.

Description에는 App을 구별할 이름을 작성하고,
Bundle ID는 역도메인 형식으로 작성한다.

아래의 리스트에서 Push Notification을 선택하고,
Continue와 Register를 눌러 작업을 완료한다.

그러면 Identifiers에 새로 추가한 항목이 추가되었다.
해당 항목을 눌러 Push Notifications 옆에 새롭게 활성화 된 Edit을 누른다.

개발자용 인증서와 배포용 인증서를 따로 생성하도록 되어있다.

이전에 Keychain에서 생성한 파일을 첨부하고 Continue를 눌러 인증서를 생성해 다운로드한다.
개발자용 인증서와 배포용 인증서의 생성 방식은 동일하다.
나머지 하나도 똑같이 생성해 다운로드하면 된다.

인증서의 설치는 더블클릭만으로 간단히 완료된다.
login의 My Ceticificates에 설치한 인증서가 존재하는지 확인하고

Export를 선택한다.

파일 형식은 Personal Information Exchange가 선택되어 있어야한다.
저장할 이름과 경로를 선택하고 Save를 누르면

두 번의 암호 입력 팝업이 표시된다.
첫번째 팝업은 Provider에 인증서를 업로드 할 때 사용된다.
두번째 팝업은 Mac 사용자의 비밀번호를 입력한다.

개발용 인증서 뿐만이 아닌 배포용 인증서도 같은 방식으로 진행한다.

이번엔 Push 서버를 구성해야한다.
실습에는 MS의 Azure를 사용한다.
무료계정을 생성해 간단하게 사용할 수 있다.

Azure의 Dashboard에서 Create New Resource를 선택한다.

이후 Notification Hub를 찾아 생성하고,
필요한 정보를 기입한다.

Resource Group과 Notification Hub Namespace는 새롭게 생성한다.
Notification Hub의 이름도 원하는 대로 작성하면 된다.
Location은 한국 Region이 존재하지 않기 때문에 가장 근처에 있는 Japan을 사용했지만 어떤 것을 사용해도 문제 없다.

생성을 마치고 Notification Hub로 들어와 APNS를 선택한다.

앞에서 생성한 인증서 중 배포용 인증서를 선택하고, 생성시 입력했던 암호를 기입하면 등록이 가능하다.

Notification Hub는 기본적으로 한개의 인증서만 사용할 수 있다.
따라서 개발용 인증서를 사용하기 위해서는 새로운 Hub를 생성해야한다.
Dashboard에서 Notification Hub Namespace를 선택하고

새 Hub를 생성한다.

마찬가지로 개발용 인증서를 등록하는데 여기서는 Application Mode를 Sandbox로 변경해야한다.

이 둘을 구분해서 사용하는 것은 권고가 아닌 필수사항이다.
주의하도록 하자.

 

Provider and Client

실습을 진행하기 전 사용할 Framework를 다운받는다.
해당 파일은 강의에서 제공되는 파일이다.

프로젝트를 새로 생성한 다음, Bundle Identifier를 이전에 Provisioning Portal에서 생성한 이름으로 변경한다.

Signing & Capability로 이동한 다음, Push Notification을 찾아 추가한다.

다운 받았던 Framework 파일의 압축을 풀어 프로젝트 파일에 복사한다.

이후 다시 General 탭에서 Frameworks, Libraries and Embeded Content 부분에 드래그해 추가한다.

해당 Framework는 Obj-C로 구현되어있다.
Swift에서 해당 Framework를 사용하려면 Bridging header를 추가하고, import 코드를 작성해야한다.

Objective-C File을 선택해 파일을 생성한다.
File Type은 Empty File이고

위와 같은 팝업이 표시 됐을 때 Create Bridging Header를 선택해야한다.

해당 과정을 통해 Bridging Header 파일이 자동으로 생성된다.
해당 파일만 필요하기 때문에 dummy 파일은 삭제한다.

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import <WindowsAzureMessaging/WindowsAzureMessaging.h>

Bridging Header 파일 안에 import 문을 작성한다.
이 과정을 통해 swift 파일에서 Framework를 사용할 수 있다.

Sanbox라는 이름의 swift 파일을 새로 생성한다.

//
//  Sanbox.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import Foundation

let hubName = "eggthem17push_dev"

Azure에서 생성한 ~dev 허브에서 이름을 복사해 새로운 상수에 저장한다.

Azure의 좌측 메뉴에서 Access Policies를 선택한다.

이후 보여주는 정책은 두가지로,
첫번째 정책은 수신 전용으로 앱에서 직접 Push를 전송해야 한다면 두번째 정책을 사용해야한다.
이번엔 첫번째 정책의 문자열을 복사한다.

//
//  Sanbox.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import Foundation

let hubName = "eggthem17push_dev"
let hubConnectionString = "Endpoint=sb://eggthem17push.servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey="

해당 문자열을 새로운 상수에 저장한다.

새로운 Swift 파일을 생성해,
해당 파일에서 Singletone 객체를 만들고, Push에 관련된 기능을 구현한다.

//
//  PushManager.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import Foundation

class PushManager: NSObject {
	static let shared =  PushManager()
	
	private override init() {
		super.init()
	}
}

해당 클래스 내에서 hub 속성을 생성하고 초기화한다.

//
//  PushManager.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import Foundation

class PushManager: NSObject {
	static let shared =  PushManager()
	
	let hub: SBNotificationHub
	
	private override init() {
		hub = SBNotificationHub(connectionString: hubConnectionString, notificationHubPath: hubName)
		
		super.init()
	}
}

SBNotificationHub의 파라미터에 Sanbox 파일에서 생성한 문자열들을 전달한다.

//
//  PushManager.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import Foundation
import UIKit
import UserNotifications

class PushManager: NSObject {
	static let shared =  PushManager()
	
	let hub: SBNotificationHub
	
	private override init() {
		hub = SBNotificationHub(connectionString: hubConnectionString, notificationHubPath: hubName)
		
		super.init()
	}
}

이후 UIKit과 UserNotifications를 import 한다.

//
//  PushManager.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import Foundation
import UIKit
import UserNotifications

class PushManager: NSObject {
	static let shared =  PushManager()
	
	let hub: SBNotificationHub
	
	private override init() {
		hub = SBNotificationHub(connectionString: hubConnectionString, notificationHubPath: hubName)
		
		super.init()
	}
	
	func setup() {
		let center = UNUserNotificationCenter.current()
		center.delegate = self
		center.requestAuthorization(options: [.sound, .alert, .badge]) { granted, error in
			if granted && error == nil {
				
			}
		}
	}
}

setup 메소드를 생성하고, 권한을 요청한다.
해당 과정은 Local Notification과 동일하다.

func setup() {
	let center = UNUserNotificationCenter.current()
	center.delegate = self
	center.requestAuthorization(options: [.sound, .alert, .badge]) { granted, error in
		if granted && error == nil {
			DispatchQueue.main.async {
				UIApplication.shared.registerForRemoteNotifications()
			}
		}
	}
}

권한을 요청한 다음에는 APNs에 기기를 등록해야한다.
RegisterForRemoteNotifications 메소드를 호출하면 APNs에 기기를 등록하고, deleget 메소드를 사용해 결과를 알려준다.

//
//  PushManager.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import Foundation
import UIKit
import UserNotifications

class PushManager: NSObject {
	static let shared =  PushManager()
	
	let hub: SBNotificationHub
	
	private override init() {
		hub = SBNotificationHub(connectionString: hubConnectionString, notificationHubPath: hubName)
		
		super.init()
	}
	
	func setup() {
		let center = UNUserNotificationCenter.current()
		center.delegate = self
		center.requestAuthorization(options: [.sound, .alert, .badge]) { granted, error in
			if granted && error == nil {
				DispatchQueue.main.async {
					UIApplication.shared.registerForRemoteNotifications()
				}
			}
		}
	}
}

extension PushManager: UNUserNotificationCenterDelegate {
	
}

extension을 생성하고 UnUsernOtificationCenterDeleget 프로토콜을 채용한다.

extension PushManager: UNUserNotificationCenterDelegate {
	func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
		completionHandler([.badge])
	}
	func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
		completionHandler()
	}
}

해당 extension에서 구현하는 메소드와 메소드가 호출되는 시점은 Local Notification과 동일하다.
지금은 Complition Handler만 호출하도록 구현한다.

//
//  AppDelegate.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {



	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
		// Override point for customization after application launch.
		
		PushManager.shared.setup()
		
		return true
	}

	// MARK: UISceneSession Lifecycle

	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)
	}

	func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
		// Called when the user discards a scene session.
		// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
		// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
	}


}

Project의 App deleget 파일을 선택하고, application(didfinishLaunchingWithOptions:) 메소드에서
setup 메소드를 호출하게끔 작성한다.

extension을 추가하고 Push Notification과 관련된 delegate 메소드를 구현한다.
application(didRegisterForRemoteNotificationsWithDeviceToken:)메소드는 APNs에 등록이 완료되면 호출된다.
두번째 파라미터에는 발급된 token이 전달된다.
해당 token을 Notification Hub로 전달해야한다.

//
//  PushManager.swift
//  Push Practice
//
//  Created by Martin.Q on 2021/12/03.
//

import Foundation
import UIKit
import UserNotifications

class PushManager: NSObject {
	static let shared =  PushManager()
	
	let hub: SBNotificationHub
	
	private override init() {
		hub = SBNotificationHub(connectionString: hubConnectionString, notificationHubPath: hubName)
		
		super.init()
	}
	
	func setup() {
		let center = UNUserNotificationCenter.current()
		center.delegate = self
		center.requestAuthorization(options: [.sound, .alert, .badge]) { granted, error in
			if granted && error == nil {
				DispatchQueue.main.async {
					UIApplication.shared.registerForRemoteNotifications()
				}
			}
		}
	}
	
	func registerDeviceToken(token: Data) {
		hub.registerNative(withDeviceToken: token, tags: nil) { error in
			if let error = error {
				print("Reg Error: \(error.localizedDescription)")
			} else {
				print("Reg Success")
			}
		}
	}
}

extension PushManager: UNUserNotificationCenterDelegate {
	func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
		completionHandler([.badge])
	}
	func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
		completionHandler()
	}
}

pushmanager에서 token을 등록하는 메소드를 구현한다.

extension AppDelegate {
	func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
		PushManager.shared.registerDeviceToken(token: deviceToken)
	}
}

다시 AppDelegate로 이동해 메소드를 호출하고 token을 전달한다.

extension AppDelegate {
	func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
		PushManager.shared.registerDeviceToken(token: deviceToken)
	}
	func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
		print(error)
	}
}

application(didFailToRegisterForRemoteNotificationsWithError:) 메소드는 기기 등록이 실패했을 때 호출된다.
이때는 보통 Push 관련 기능을 비활성화 하는 코드를 등록한다.
지금은 오류를 출력하도록 구현한다.

여기까지가 Push Notification을 사용하기위한 최소한의 작업이다.
Simulator에서는 Push를 받을 수 없으므로 실제 기기에서 테스트해야한다.