호댕의 iOS 개발

[iOS] 푸쉬 알림(Push Notification) 어떻게 할까? (OneSignal, FCM, Foreground에서 알림 받지 않기 등등) 본문

Software Engineering/iOS

[iOS] 푸쉬 알림(Push Notification) 어떻게 할까? (OneSignal, FCM, Foreground에서 알림 받지 않기 등등)

호르댕댕댕 2022. 9. 29. 21:55

회사에서 푸쉬 알림 관련 작업을 맡게 되었다. 

이전부터 푸쉬 알림은 어떻게 하나 궁금했었는데 때마침 관련 작업을 맡게 되면서 새롭게 알게 된 내용을 정리해보고자 한다. 

 

특히 푸쉬 알림은 사용자들이 꾸준하게 앱을 사용할 수 있도록 유도하는 중요한 툴인 만큼 중요한 기능이라 생각된다. 

 

(해당 글은 처음부터 푸쉬 알림을 등록하는 방법보다는 푸쉬 알림과 세부적인 커스텀?에 대해 다루고 있습니다. 푸쉬 알림을 처음부터 등록하는 것은 추후 정리해보겠습니다)

 

iOS에서의 푸쉬 알림

일단 기본적으로 iOS에서 푸쉬 알림이 어떤 식으로 동작하는지를 살펴보자. 

 

먼저 푸쉬 알림은 우리가 잠금 화면에서도 특정 앱의 이벤트를 알 수 있도록 전달되는 알림을 의미한다.

이런 식으로 푸쉬 알림이 오는 것은 iOS를 한 번이라도 사용했다면 흔히 볼 수 있었을 것이다. 

iOS 12 이후로는 이렇게 푸쉬 알림을 그룹화하는 것도 가능해졌다. 

(그룹화 관련해선 아래에서 좀 더 자세히 알아보자)

 

iOS에선 APNs(Apple Push Notification service)를 사용해 써드 파티 앱을 만드는 개발자들도 앱에 푸쉬 알림을 보낼 수 있도록 하고 있다. 

서비스의 서버에서 직접 알람을 보내지 않고 APNs를 거쳐서 푸쉬알림을 보낼 수 있도록 해놓은 것이다. 

 

푸쉬 알림 그룹화 

이번 내가 맡은 구현에선 채팅방에 따라 메세지를 다른 그룹으로 묶일 수 있도록 해야 했다. 

일단 해당 애플 공식 문서를 참고했다. (remote notification을 어떻게 구현하는지에 대한 문서이다)

 

그리고 하단 Payload key reference를 살펴보면 thread-id라는 key를 찾을 수 있다. value는 String으로 가지며, grouping과 관련된 앱의 특정 식별자라고 소개를 하고 있다. 이는 UNNotificationContentthreadIdentifier와 연관이 되어 있다. 

An app-specific identifier for grouping related notifications. This value corresponds to the threadIdentifier property in the UNNotificationContent object.

 

그렇다면 어떻게 ThreadIdentifier를 지정할 수 있을까?

 

OneSignal

일단 OneSignal 먼저 살펴보자. 

공식문서를 보면 아래 이미지처럼 설명이 나와있다. 

payload에 thread_id, summary_arg, summary_arg_count를 담아서 보내면 그룹화가 가능하다는 것이다. 

 

그럼 API 문서를 통해 좀 더 해당 파라미터 들에 대해 자세히 알아보자. 

일단 푸쉬 알림 그룹화 기능이 iOS 12 이후로 나온 만큼 해당 파라미터들도 모두 iOS 12 이상에서만 사용할 수 있다.

 

  • thread_id : 이를 통해 관련된 알림이 하나의 그룹으로 묶일 수 있도록 해준다. 만약 두 개의 알림이 같은 thread_id를 가지고 있다면 하나의 그룹으로 묶이게 된다. 
  • summary_arg : 위 파라미터를 사용하게 되면 그룹화된 알림을 보낼 수 있는데 이를 통해 누가 해당 알림을 보냈는지에 대해서도 커스텀할 수 있다. 
  • summary_arg_count : 이 파라미터를 사용하면 몇 개의 알림을 보냈는지도 커스텀할 수 있다. 이를 통해 기존에 보내졌던 알림에 해당 수를 더해서 알림이 왔다고 보내진다. (다만 이를 사용할 이유가 있는지 명확히 모르겠다)

 

다만 summary_arg, summary_arg_count를 직접 사용해도 설명처럼 적용이 되진 않았다... 🥲

대부분의 상용앱들도 단순히 앱 이름이 나오도록 구현을 했었는데 따로 summary_arg를 적용하지 않더라도 앱 이름이 잘 나왔기 때문에 이는 사용하지 않았다. 

 

이를 통해 메세지가 보내진 채팅 방이 다르면 thread_id를 다르게 주는 방식으로 채팅방 별 채팅을 그룹화 할 수 있도록 구현했다. 

{
    "include_external_user_ids": ["{유저 ID 입력}"],
    "app_id": "{앱 ID 입력}",
    "headings": {"en": "제목"},
    "contents": {"en": "내용"},
    "url": "{url}", // 딥링크 주소를 전달
    "small_icon": "{알림 좌측에 뜨는 작은 이미지(앱로고)}",
    "ios_badgeType": "Increase",
    "ios_badgeCount": "1", // 앱 아이콘에 뜨는 빨간 동그라미로 된 숫자 의미
    "thread_id": "{그룹화를 위해 필요한 식별자}",
}

포스트맨을 통해 직접 이를 보내봤더니 thread_id에 따라 잘 그룹화가 되는 것을 확인할 수 있었다. 

 

FCM

다만 문제는 FCM(Firebase)이었다. FCM은 따로 thread_id를 페이로드에 보낼 수 없었다. 만약 페이로드에 thread_id를 실어서 보내더라도 알림을 받을 때에는 threadIdentifier가 적용이 되지 않았다. 

 

그래서 원시그널처럼 간단하게 페이로드에 thread-id 관련 Key 값을 추가한다고 해서 그룹화를 해줄 수 없었다. 

 

그래서 찾은 방법들은 다음과 같다. 

 

UNNotificationServiceExtension을 통해 앱이 푸쉬를 받고 보여주기 전에 threadIdentifier에 값을 할당해줘서 서로 다른 그룹이 되도록 구현하는 것이다. 

 

일단 UNNotificationServiceExtension은 클래스로 remote notification을 사용자에게 전달하기 전에 내용을 수정할 수 있도록 하는 객체이다. 

 

이는 자체 UI를 제공하진 않으며 만약 UI까지 커스텀하기 위해선 UNNotificationContentExtension을 통해 해줘야 한다. 이를 통해 알림 컨텐츠를 수정하거나 앱과 관련된 컨텐츠를 다운로드할 수 있도록 하며, 암호화가 되어 있는 데이터를 풀 수 있다. 

해당 객체는 직접 생성하진 않으며 Xcode 템플릿을 통해 생성을 하게 되면 이를 상속받은 하위 클래스로 사용을 하게 된다. 

 

다만 이를 사용하기 위해선 반드시 payload에 mutable-content1로 줘야 하며 원격 알림을 받을 수 있도록 알림을 켜놔야 한다. 

 

일단 Xcode를 통해 해당 하위 클래스를 추가하는 방법은 다음과 같다. 

 

1. Xcode > File > New.. > Target을 선택한다. 

2. Target에서 Notification Service Extension을 선택한다. 

이렇게 하게 되면 새로운 타겟이 추가되게 되고 관련 소스파일들이 생성된다. 

 

override func didReceive(
	_ request: UNNotificationRequest, 
    withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
)

그럼 이런 메서드가 생성되고 이는 알림을 받고 유저에게 보내기 전에 호출된다. 

 

그리고 하단에 있는 메서드의 경우 미쳐 didReceive에서 처리하지 못한 것들을 마지막으로 처리할 수 있도록 있는 메서드이다. 

override func serviceExtensionTimeWillExpire()

그래서 나는 payload의 data 쪽에 thread-id를 보낸 다음 didReceive에서 request를 받아서 threadIdentifier를 지정해주는 방식으로 구현을 해보려 했다. 

 

UNNotificationRequest의 경우 UNNotificationContent를 가지고 있기 때문에 UNNotificationContent의 프로퍼티 threadIdentifier를 사용할 수 있었기 때문이다. 

 

하지만 문제는 UNNotificationServiceExtension의 메서드들이 아무리 해도 호출이 되지 않았다. mutable-content도 true(1)로 잘 오는지 확인했지만, true로 오더라도 호출이 안됐다...

 

참고로 mutable-content를 1로 보내는 것도 많이 헤멨다... Firebase 상 설명으로 하면 제대로 알림도 오지 않아 다음처럼 페이로드를 보냈다.

{
  "to":"{푸쉬 토큰}",
  "mutable_content" : true, // 대쉬가 아닌 언더바로 작성해주니 해결됨. 
  "notification": {
    "body": "push_body",
    "title": "push_title",
    "sound": "default", // sound를 따로 보내지 않으면 아무 소리도 나지 않음
    "badge": 100, // 이렇게 보내면 한 번 알림을 보낼 경우 뱃지가 100으로 바로 올라감.
    "activity": "AlarmActivity"
  },
  "data": {
    "thread-id": {임의의 thread id 값}
  }
}

내용의 경우 notification을 key값으로 보내야 한다. apns로 보내면 아예 알림이 오지 않는다.

 

그래서 FCM을 사용해서 알림을 그룹핑하는 작업은 실패했다... 

(혹시 방법을 아시는 분은 알려주세요...)

 

알림은 그럼 어디서 처리되나?

알림을 커스텀해봤으니 알림을 어디서 처리하는지도 문제였다. 

이는 UNUserNotificationCenterDelegate를 채택한 곳에서 처리를 할 수 있었다. 

 

여기서 주의할 점은 UNUserNotificationCenter 객체의 delegate는 반드시 application(_:willFinishLaunchingWithOptions:)나 최소한 application(_:didFinishLaunchingWithOptions:)에서는 채택을 해줘야 하는 것이다. 이 이후에 채택을 할 경우 오는 알림을 놓칠 수도 있다. 

 

UNUserNotificationCenter.current().delegate = self

그리고 알림을 사용자가 허용할 것인지에 대해서도 application(_:didFinishLaunchingWithOptions:)에서 해줘야 한다. 

let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
    options: authOptions,
    completionHandler: { _, _ in }
)

이렇게 해주고 사용자가 알림을 허용했다면 앱이 Foreground인 경우 알림이 왔을 때와 알림을 사용자가 탭해서 열었을 때에 대한 처리를 UNUserNotificationCenterDelegate의 메서드에서 해줄 수 있다. 

 

앱이 foreground에 있을 때 알림이 온 경우 처리

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (NotificationPresentOptions) -> Void
)

위 함수를 통해 앱이 foreground에 있을 때 알림에 대한 처리를 해줄 수 있다. 

 

알림을 사용자가 탭해서 연 경우 처리

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
)

사용자가 알림을 탭해서 앱을 실행하도록 한 경우 해당 메서드가 호출된다. 

 

여기서 서버에서 보내준 deeplink 주소를 추출해서 특정 view로 이동하도록 구현을 해줄 수 있다. 

 

앱이 Foreground에 있을 때 앱의 알림을 받지 않도록 하고 싶다면...?

특정 알림의 경우 앱이 Foreground에 있을 때 알림이 계속해서 온다면 사용자의 피로감을 올릴 수 있다는 판단 하에 해당 경우 알림을 받지 않도록 구현을 해야 했다. 

 

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (NotificationPresentOptions) -> Void
)

처음에는 위 메서드를 아예 구현하지 않는다면 알림이 오지 않을거라 생각했다. 

그렇다면 completionHandler로 알림 옵션에 대해 보내지 않기 때문에 아무런 알림이 오지 않는다고 판단했기 때문이다. 

NotificationPresentOptions의 경우 위 그림처럼 alert, sound, badge, banner, list로 구성되어 있으며 alert는 그림처럼 알림을 보여준다는 의미이고 sound는 말 그대로 소리가 난다는 것이다. sound는 따로 지정해주지 않는다면 기본 소리로 나게 된다. 

 

하지만 아예 위 함수를 작성하지 않는다고 해서 알림이 안오는 건 아니었다. 

따로 처리를 해주지 않는다면 기본적으로 alert와 sound 전부 활성화되어 있는 것 같다. 

 

그래서 notification을 통해 받은 payload를 파싱해서 어떤 알림인지 파악할 수 있도록 했고 조건문을 통해 특정 알림인 경우 completionHandler에 빈 배열을 전달하도록 했다. 

 

이렇게 했더니 특정 경우에만 알림을 받지 않을 수 있었다. 

 


아직 완전히 푸쉬 알림 전체를 구현해본 것은 아니지만 푸쉬 알림 관련 구현을 하면서 푸쉬 알림이 기본적으로 어떤 식으로 이뤄지고 각 상황에서 어떤 함수들이 호출되는지 배울 수 있는 좋은 기회였다. 사이드로 하고 있는 프로젝트에서도 기회가 되면 푸쉬 알림을 적용해서 처음부터 끝까지 완전히 이해해보고 싶다. 

 

 

 

참고 자료

 https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns/

 

Apple Developer Documentation

 

developer.apple.com

https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification

 

Apple Developer Documentation

 

developer.apple.com

https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension

 

Apple Developer Documentation

 

developer.apple.com

https://documentation.onesignal.com/reference/push-channel-properties

 

Push Channel Properties

If you are sending push notifications, use the following parameters. Read more in supported languages. ParameterTypePlatformDescriptioncontentsobjectAll - PushRequired unless content_available=true or template_id is set.The notification's content (excludin

documentation.onesignal.com

https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/1649518-usernotificationcenter

 

Apple Developer Documentation

 

developer.apple.com

 

Comments