일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- SwiftUI
- dfs
- 알고리즘
- Kotlin
- 프로그래머스
- Algorithm
- 파이썬 풀이
- error
- Level 1
- 백준 온라인 저지
- 그리디 알고리즘
- 파이썬
- greedy algorithm
- Android
- Swift공부
- iOS개발
- UIKit
- 백준온라인저지
- ios
- 정렬
- 공부
- 알고리즘 공부
- 앱개발
- 오토레이아웃
- Clean Architecture
- Autolayout
- swift
- 안드로이드 공부
- Python
- BFS
- Today
- Total
Tori의 개발 공부
FCM 푸시알림 사용기1 - 특정 사용자에게 푸시알림 보내기 본문
프로젝트를 진행하면서 FCM을 이용한 푸시 알림을 구현하게 되었다.
고민했던 부분을 바탕으로 구현 내용을 정리해
참고로 프로젝트는 백엔드 개발 없이 파이어베이스의 firestore 서비스를 이용했다. 또한 파이어베이스 등록 및 인증키 등록 과정은 다른 블로그들에 자세히 나와있어 코드 구현 부분 중심으로 작성할 예정이다.
원격 알림 등록, FCM 토큰 얻기
Notification 서비스 함수에 다음과 같은 등록 함수를 작성하고
func registerRemoteNotification() {
if #available(iOS 10.0, *) {
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: {_, _ in })
} else {
let settings: UIUserNotificationSettings =
UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
UIApplication.shared.registerUserNotificationSettings(settings)
}
UIApplication.shared.registerForRemoteNotifications()
}
- iOS 10.0 이상의 버전 **UNUserNotificationCenter**의 requestAuthorization 메서드를 사용하여 알림 권한을 요청할 때 사용할 옵션을 설정
- iOS 10.0 미만의 버전 **UIUserNotificationSettings**를 사용하여 알림 권한을 요청하고 설정 (사실상 앱 타겟이 15.0 이상이기 때문에 실행될 일은 없다.)
알림에 대한 알림 창, 배지, 사운드에 대해 설정 후 권한을 요청해준다.
AppDelegate의 didFinishLaunchingWithOptions 함수에 다음과 같이 작성해 주었다.
import UIKit
import FirebaseCore
import FirebaseMessaging
import UserNotifications
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
...
FirebaseApp.configure()
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self
UIApplication.shared.registerForRemoteNotifications()
...
return true
}
...
}
권한 요청을 앱 시작점이 아닌 로그인 후 홈 화면에 들어갔을 때 띄울 예정이라 따로 서비스 함수에 작성하였지만,
개발하면서 여러 이유로 다시 앱을 시작하면 권한 요청을 받도록 수정되어 앱 델리게이트 함수에 적어두었다.
따라서 현재 상황의 경우 굳이 함수로 뺄 이유는 없을 수 있지만
푸시 알림 관련 함수를 모두 서비스 클래스에 모아두었기 때문에 재사용성 및 모듈화 부분의 이점을 챙길 수 있도록 남겨두었다.
FCM 토큰 얻기
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
guard let token = fcmToken else { return }
print("FCM등록 토큰 : \\(token)")
let dataDict: [String: String] = ["token": token]
NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict)
}
}
MessagingDelegate 프로토콜에서 요구하는 메서드이며 FCM에서 푸시 알림 등록 토큰을 수신할 때 호출된다.
FCM 토큰은 기기마다 고유한 값으로 특정 사용자(디바이스)에게 알림을 보낼 때 토큰 값으로 사용자를 식별하게 된다.
토큰 값은 앱을 재설치하거나 사용자가 디바이스에서 앱의 데이터를 지울 경우에 재생성될 수 있다.
그러곤 Notification 서비스 함수에 다음과 같이 작성해 주었다.
func saveToken() {
Messaging.messaging().token { token, error in
if let error = error {
print("토큰 가져오기 실패 : \\(error)")
return
} else if let token = token {
let newToken = Token(fcmToken: token, badgeCount: 0)
if UserDefaultsManager.shared.isLogin() {
FirestoreService.shared.saveDocument(collectionId: .tokens, documentId: UserDefaultsManager.shared.getUserData().userId, data: newToken) { result in
switch result {
case .success:
print("토큰 저장 성공 Token: \\(token)")
case .failure(let error):
print("토큰 저장 실패: \\(error)")
}
}
}
}
}
}
우선 토큰 값을 저장하는 모델 구조는 다음과 같다.
struct Token: Codable {
let fcmToken: String
let badgeCount: Int
}
처음에는 토큰값만 저장했었지만, 이후 뱃지 카운팅 핸들링을 위해 현재 사용자의 뱃지 카운팅 값도 같이 저장하였다.
이때 앱델리게이트에서 호출한 saveToken()은 자동로그인 시 토큰이 저장되도록 하기 위함이다.
saveToken에서 UserDefaultsManager.shared.isLogin() 로그인 여부를 확인하고 로그인이 되어있다면 그 유저 id값을 이용해 저장한다.
만약 로그인이 되지 않은 상태라면 토큰이 저장되지 않는다.
따라서 이 경우는 로그인 시 saveToken()을 한 번 더 호출해 주어 로그인을 하여 접속할 때도 토큰을 저장할 수 있도록 하였다.
앱이 실행되고 호출되기 때문에 뱃지카운트는 0으로 설정한다.
로그인을 할 때마다 (자동로그인 포함) 사용자 id값에 현재 디바이스의 토큰을 저장한다.
푸시 알림 보내기
서비스 함수에 sendNotification 함수를 정의하였다.
함수 파라미터는 다음과 같이 정의되어 있다.
- userId: 알림을 받을 UserId
- sendUserName: 보내는 사람(자신) 닉네임
- notiType: 노티 타입
- like : 사용자가 좋아요를 눌렀을 때
- message: 사용자가 메시지를 보냈을 때
- matching: 다른 사용자와 매칭이 됐을 때
- notiType은 푸시알림이 울리는 상황을 정의하였다.
- messageContent: 메시지의 경우 메시지 내용
sendNotification 전체 코드 보기
/// 푸쉬알림 보내기
/// userId: 알림을 받을 UserId, sendUserName: 보내는 사람(자신) 닉네임, notiType: like, message, matching
func sendNotification(userId: String, sendUserName: String, notiType: PushNotiType, messageContent: String? = nil) {
let title: String = "Pico"
var subTitle: String?
var body: String = ""
FirestoreService.shared.loadDocument(collectionId: .tokens, documentId: userId, dataType: Token.self) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let data):
switch notiType {
case .like:
body = "\(sendUserName)님이 좋아요를 누르셨습니다."
case .message:
subTitle = "\(sendUserName)님이 메시지를 보냈습니다."
body = messageContent ?? "메일함을 확인해보세요."
case .matching:
body = "\(sendUserName)님과 매칭이 되었습니다."
}
guard let token = data else { return }
let urlString = "https://fcm.googleapis.com/fcm/send"
let url = NSURL(string: urlString)!
var paramString: [String: Any] = ["to": token.fcmToken,
"notification": ["title": title, "body": body, "sound": "default", "badge": token.badgeCount + 1],
"data": ["title": title, "body": body],
"content_available": true
]
if let subTitle = subTitle {
paramString["notification"] = ["title": title, "body": body, "sound": "default", "badge": token.badgeCount + 1, "subtitle": subTitle]
}
let request = NSMutableURLRequest(url: url as URL)
request.httpMethod = "POST"
request.httpBody = try? JSONSerialization.data(withJSONObject: paramString, options: [.prettyPrinted])
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("key=\(Bundle.main.notificationKey)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, _, _) in
do {
if let jsonData = data {
if let jsonDataDict = try JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: AnyObject] {
NSLog("Received data:\n\(jsonDataDict))")
}
}
} catch let err as NSError {
print(err.debugDescription)
}
}
task.resume()
updateBadgeCount(userId: userId)
case .failure(let error):
print("토큰 불러오기 실패: \(error)")
}
}
}
페이로드 구조
var paramString: [String: Any] = ["to": token.fcmToken,
"notification": ["title": title, "body": body, "sound": "default", "badge": token.badgeCount + 1],
"data": ["title": title, "body": body],
"content_available": true
]
if let subTitle = subTitle {
paramString["notification"] = ["title": title, "body": body, "sound": "default", "badge": token.badgeCount + 1, "subtitle": subTitle]
}
페이로드를 나타내는 딕셔너리이다. 메시지의 경우만 메시지 내용을 보여주기 위해 subTitle을 추가하였다.
- to: 메시지를 받을 FCM 토큰
- notification: 푸시 알림의 내용. "title", "body", "sound", "badge”, “subtitle”(메시지의 경우만)를 추가하였다.
- title: 푸시 알림 상단에 뜨는 타이틀
- body: 푸시알림 본문
- sound: 알림 소리 (default로 설정 시 앱 기본 알림음으로 감)
- badge: 푸시 알림 수신 시 앱 아이콘에 표시될 배지 숫자를 지정
- subtitle: 푸시 알림에 서브 타이틀 지정 본문 위에 볼드체로 위치함 메시지의 경우 메시지 내용도 같이 표현하기 위해 기본 “메시지를 보냈습니다.” 를 서브타이틀로 지정하고 body로 메시지 내용을 기록하였다
- data: 추가적인 데이터를 담는 딕셔너리
- notification : 사용자에게 직접적으로 보여지는 정보
- data: 사용자에게 직접적으로 보여지지 않지만 앱 내부에서 사용될 추가적인 정보, 개발자가 자유롭게 정의
- notification VS data
- content_available : 메시지가 사용 가능한지 여부, true로 설정해줌
FCM서버로 요청 보내기
let urlString = "<https://fcm.googleapis.com/fcm/send>"
let url = NSURL(string: urlString)!
let request = NSMutableURLRequest(url: url as URL)
request.httpMethod = "POST"
request.httpBody = try? JSONSerialization.data(withJSONObject: paramString, options: [.prettyPrinted])
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("key=\\(Bundle.main.notificationKey)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, _, _) in
do {
if let jsonData = data {
if let jsonDataDict = try JSONSerialization.jsonObject(with: jsonData, options: JSONSerialization.ReadingOptions.allowFragments) as? [String: AnyObject] {
NSLog("Received data:\\n\\(jsonDataDict))")
}
}
} catch let err as NSError {
print(err.debugDescription)
}
}
task.resume()
URLSession을 통해서 FCM 서버로 요청을 보낸다.
푸시알림 서비스의 Key의 경우 유출 방지를 위해 따로 plist에 저장해 두어 gitignore에 추가하여 깃이 추적하지 않도록 하고 Bundle 익스텐션에 프로퍼티로 정의해 두어 코드에서 사용할 수 있도록 하였다.
(URLSession의 공부 내용은 따로 정리해서 올릴 예정 - 추후 링크 첨부)
'IOS App Programming > IOS 연습' 카테고리의 다른 글
FCM 푸시알림 사용기3 - 푸시 알림 클릭 핸들링 (0) | 2024.02.21 |
---|---|
FCM 푸시알림 사용기2 - 뱃지 카운팅 하기 (0) | 2024.02.21 |
Notifiaction Center VS Delegate Pattern (1) | 2024.02.13 |
[IOS] UserDefaults란? (0) | 2023.04.12 |
[IOS] 카메라, 갤러리 접근해서 사진 가져오기 - UIImagePickerController (0) | 2023.04.06 |