본문 바로가기

iOS/니카내카

[니카내카] Push Notification을 구현해보자.

728x90

푸시알림을 구현할 때 가장 많이 사용하는 provider server는 FCM이다.

그러므로 firebase에서 프로젝트를 만드는 것부터 시작해보자!

 

#Firebase Project 생성 

firebase의 console로 이동한다.

그리고 나서 아래와 같이 프로젝트 추가를 선택한다.

1개의 앱에 1개의 프로젝트로 연결되는 것은 아니다. 파이어베이스 프로젝트 하나에 여러 앱이 연결되어서 관리할 수도 있다.

그래서 기존에 만들어두었던 SeSAC 프로젝트에 할까? 하다가 .. 혹시 몰라서 니카내카 앱 전용 프로젝트를 만들기로 했다.

 

위와 같이 프로젝트 이름을 작성하면 된다.

 

그대로 두고 계속을 누르면 

 

계정을 선택하라고 나오는데, 무료 버전을 사용하기 때문에 Default 계정을 선택한 다음 프로젝트 만들기를 누르면 된다.

 

약간의 로딩 시간이 걸린 뒤 계속 버튼이 활성화되면 누르면

 

위와 같이 프로젝트가 만들어진 것을 확인할 수 있다.

 

#Firebase와 프로젝트 연동 

위의 과정을 통해 만든 파이어베이스 콘솔 프로젝트와 엑스코드에서 작업 중인 프로젝트를 연동해야 서로 연결되어서 사용할 수 있다.

 

여기서 원하는 앱을 선택해서 시작하면 된다.

 

니카내카는 iOS 앱이므로 iOS+ 버튼을 눌러서 시작하면 된다.

순서대로 작성하면 된다.

 

✔️ 앱 번들 ID를 작성하고

✔️ App Store에 등록되어 있다면 App Store ID도 작성한다. 

 

GoogleService-Info.plist 파일도 다운로드 받은 뒤, 프로젝트 아래에 넣어준다.

위와 같이 설정해서 넣어주면 된다.

 

그리고 나서 firebase sdk를 설치하면 된다.

사이트에 나와 있는 링크 https://github.com/firebase/firebase-ios-sdk 를 복사한 뒤에 아래와 같이 package dependencies로 가서 추가하면 된다.

모두 다 설치하는 것이 아니라 필요한 것만 골라서 설치하면 된다.

 

 

용량이 크기 때문에 .. 추가하는데 시간이 조금 걸릴 수 있다 ..

마음을 여유롭게 가지고 .. 기다린다 .. (그러나 인텔에게 기다림이란 .. 약간의 고통인 것 .. )

 

조금 기다리고 나면 위와 같이 많은 파일들이 추가된 것을 볼 수 있다.

 

그리고 초기화 코드를 AppDelegate에 추가한다.

 

하라는대로 고대로 .. 똑같이 복사+붙여넣기 하면 된다.

(잘 설치되었는지, 확인하기 위해서 위의 과정까지 하고 한번 빌드해볼 것 .. !!)

 

그리고 콘솔로 이동하면, 앱이 등록된 것을 볼 수 있다.

 


이제부터 .. 푸시 알림을 위한 본격적인 준비를 해보자 .. !!

(위의 과정은 본격적인게 아니란건가?! ㅇㅇ. 이제부터 시작임.)

 

#Xcode Capability 설정 

엑스코드의 Target > Signing & Capabilities > +Capability를 선택해서 원하는 작업을 추가하면 된다.

 

푸시 알림을 추가해야 하므로 위와 같이 Push Notification을 검색해서 drag & drop을 하거나 더블 클릭하면 

위와 같이 추가된다.

 

푸시 알림의 경우 백그라운드 상태에서도 알림을 받을 수 있어야 하므로 

Background Modes도 추가한다.

 

그리고 여러 모드 중에서 아래서 네번째에 위치한 원격 알림(Remote notifications)를 체크한다.

 

#인증키 발급

인증키 발급의 경우, 개발자 계정이 있는 사람에 한해서 발급을 받을 수 있다.

(= 개발자 계정을 구매해야 한다.)

 

https://developer.apple.com/account/

 

로그인 - Apple

 

idmsa.apple.com

 

만약 개발자 계정이 있다면, 본인의 개발자 계정으로 위의 사이트에 로그인 하면 된다.

그리고 위의 인증서, 식별자 및 프로파일 부분에서 키(영문) 탭으로 들어가면 된다.

위와 같은 화면이 나타나게 되는데, 여기서 인증키를 발급 받은 뒤 (토큰 기반 방식 사용) 파이어베이스에 연동해서 사용하면 된다.

 

인증키를 발급 받을 때는 몇가지 주의할 점이 있다. 

여기서 한가지 주의할 점이 있다면, 인증키의 경우 하나의 개발자 계정으로 최대 2개까지 발급받을 수 있다.
하나의 키로 하나의 앱을 등록하는 것은 아니기 때문에 인증키를 발급 받은 뒤에 여러 곳에서 사용해도 된다.

또한, 한번 발급받을 때 다운로드를 하지 않으면 다시 재다운로드가 불가하므로 최초 발급 시 다운로드를 하고 잘 저장해두어야 한다. (보통 노션/슬랙 등과 같은 협업 툴에 저장하거나, 따로 로컬 폴더로 관리한다.)

 

발급을 받았다면, 아래 이미지처럼 확장자가 .p8인 파일을 다운로드 할 수 있다.

이 파일을 갖고 다시 파이어베이스로 이동해서 업로드를 해야한다.

 

#Firebase에 .p8 파일 업로드 

위에서 만들었던 프로젝트 콘솔로 이동한 뒤, 프로젝트 설정으로 이동한다.

 

 

클라우드 메시징 탭으로 이동하고 아래로 조금 내리면 파일을 업로드할 수 있는 공간을 볼 수 있다.

여기서 인증서가 아니라!! APN 인증키를 업로드하면 된다.

 

위의 이미지에서 볼 수 있는 것처럼 파일을 업로드하고 > 파일 이름에서 확인할 수 있는 키 ID를 오타 없이 작성한 다음 > 애플 개발자 계정에서 확인할 수 있는 팀 ID를 마찬가지로 오타 없이 작성하면 된다. 

 


다시 엑스코드로 돌아와서, 권한/원격 알림 요청 및 기타 원격 알림과 관련된 코드를 작성하면 (= 갱신 .. 분기처리 등 .. ) 원격 알림을 보내고 받을 수 있다.

 

#권한/알림 요청 코드 작성 

messaging으로 이동해서 우측 상단에 있는 문서로 이동을 클릭해보면, 어떻게 해야 하는지 자세히 나와 있다.

 

참여 > 클라우드 메시징으로 이동한다.

https://firebase.google.com/docs/cloud-messaging/ios/first-message?hl=ko&authuser=0 

 

백그라운드 Apple 앱에 테스트 메시지 보내기  |  Firebase 클라우드 메시징

2022년 10월 18일에 오프라인과 온라인으로 진행될 Firebase Summit에 참여하세요. Firebase로 앱을 빠르게 개발하고 안심하고 앱을 출시하며 손쉽게 확장하는 방법을 알아보세요. 지금 등록하기 의견 보

firebase.google.com

 

이 페이지를 확인하면 앱에 테스트 메시지를 보낼 수 있다.

 

AppDelegate.swift 로 이동해서 아래와 같이 코드를 작성한다.

import UIKit

import FirebaseCore
import FirebaseMessaging

@main
class AppDelegate: UIResponder, UIApplicationDelegate {



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        
        // 원격 알림 시스템에 앱을 등록
        if #available(iOS 10.0, *) {
            // For iOS 10 display notification (sent via APNS)
            UNUserNotificationCenter.current().delegate = self
            
            let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
            UNUserNotificationCenter.current().requestAuthorization(
                options: authOptions,
                completionHandler: { _, _ in }
            )
        } else {
            let settings: UIUserNotificationSettings =
            UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
            application.registerUserNotificationSettings(settings)
        }
        
        application.registerForRemoteNotifications()
        
        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.
    }

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        // 세로방향 고정
        return UIInterfaceOrientationMask.portrait
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    // 재구성 사용 중지됨: APNs 토큰과 등록 토큰 매핑
    func application(application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken
    }
    
    // foreground 알림 수신: 로컬 푸시와 동일
    // 카카오톡: 후리방구와의 채팅방, 푸시마다 설정, 화면마다 설정 ..
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        
        // .banner, .list, iOS 14+
        completionHandler([.badge, .sound, .banner, .list])
    }
    
    // 유저가 푸시를 클릭했을 때만 수신확인 가능
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        print("🍋 사용자가 푸시를 클릭했습니다. 🍋")
    }
}

 

#메시지 대리자 설정 

import UIKit

import FirebaseCore
import FirebaseMessaging

@main
class AppDelegate: UIResponder, UIApplicationDelegate {



    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        
        // 원격 알림 시스템에 앱을 등록
        if #available(iOS 10.0, *) {
            // For iOS 10 display notification (sent via APNS)
            UNUserNotificationCenter.current().delegate = self
            
            let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
            UNUserNotificationCenter.current().requestAuthorization(
                options: authOptions,
                completionHandler: { _, _ in }
            )
        } else {
            let settings: UIUserNotificationSettings =
            UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
            application.registerUserNotificationSettings(settings)
        }
        
        application.registerForRemoteNotifications()
        
        // 메시지 대리자 설정
        Messaging.messaging().delegate = self
        
        // 현재 등록된 토큰 가져오기
        Messaging.messaging().token { token, error in
            if let error = error {
                print("Error fetching FCM registration token: \(error)")
            } else if let token = token {
                print("FCM registration token: \(token)")
            }
        }
        
        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.
    }

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        // 세로방향 고정
        return UIInterfaceOrientationMask.portrait
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    // 재구성 사용 중지됨: APNs 토큰과 등록 토큰 매핑
    func application(application: UIApplication,
                     didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken
    }
    
    // foreground 알림 수신: 로컬 푸시와 동일
    // 카카오톡: 후리방구와의 채팅방, 푸시마다 설정, 화면마다 설정 ..
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        
        // .banner, .list, iOS 14+
        completionHandler([.badge, .sound, .banner, .list])
    }
    
    // 유저가 푸시를 클릭했을 때만 수신확인 가능
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        print("🍋 사용자가 푸시를 클릭했습니다. 🍋")
    }
}

extension AppDelegate: MessagingDelegate {
    // 토큰 갱신 모니터링: 토큰 정보가 바뀔 때 (언제? 왜?)
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        print("Firebase registration token: \(String(describing: fcmToken))")
        
        let dataDict: [String: String] = ["token": fcmToken ?? ""]
        NotificationCenter.default.post(
            name: Notification.Name("FCMToken"),
            object: nil,
            userInfo: dataDict
        )
        // TODO: If necessary send token to application server.
        // Note: This callback is fired at each app startup and whenever a new token is generated.
    }
}

 

 

#SceneDelegate에서 초기화

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
        
        UIApplication.shared.applicationIconBadgeNumber = 0 // 배지 숫자 초기화
        UNUserNotificationCenter.current().removeAllDeliveredNotifications() // Stack의 메시지 정보 모두 초기화
    }

앱에 접속했을 때 배지 및 메시지 스택을 모두 초기화 하도록 한다.

 


어떻게 테스트를 하는가?

메시징을 하나 보내면 된다.

 

캠페인 만들기를 선택한 뒤 Firebase 메시지 온보딩에서 Firebase 알림 메시지를 선택한 뒤 푸시 알림을 생성할 수 있다.

 

위와 같이 알림 제목과 알림 텍스트를 입력한 다음에 

우측 상단의 테스트 메시지 전송 버튼을 누르면 기기에 푸시 알림이 도착하게 된다.

 

위와 같은 이미지처럼 푸시 알림이 오는 것을 볼 수 있다.

 

만약 예약된 시간에 푸시 알림을 보내고 싶다면,

앱을 선택한 다음

 

예약 시간을 지금이 아닌, 특정한 날짜의 특정한 시간으로 설정하면 된다.

 

 

추가적으로 애널리틱스 옵션과 푸시 알림 관련 설정을 선택한 다음 검토를 누르면 

위와 같은 팝업 화면이 나타나고 게시를 누르면 된다.

 

잘 게시가 되었다면, 활성 상태로 변화된다.

그리고 알림이 제대로 가면, 완료 상태로 바뀐다.

 

*지금이라고 해도 .. 한 10분 내외의 시간이 걸리기 때문에 테스트 메시지를 보내고 잘 간다면 ..

캠페인을 만들고 10분정도 기다려보는 것이 좋다 .. (인내심을 기르자.)


이렇게 하면 ... 아주 아주 ~~ 기본적인 푸시 알림 관련 설정을 한 것이다 .. 

이 글에 적은 것 이외로 푸시알림은 생각보다 분기처리가 엄청 많고 .. 고려해야 할 사항이 많기 때문에 .. 조금 더 깊게 살펴볼 필요가 있다. 

 

 

왈도 .. 푸시 알림이 메인 기능인 서비스이기 때문에 .. 이번에 공부한 것을 바탕으로 좀 더 .. 자세하게 코드를 뜯어봐야겠다 ..