Clover
article thumbnail

개요

Remote Push Notification을 이용해서 요구사항을 구현하기 위해 Firebase Cloud Messaging(FCM)을 사용해 봤습니다.

체감상 가장 레퍼런스도 많고, 알람 횟수에 상관없이 무료라는 장점이 있어서 FCM을 선택했습니다.

 

FCM을 사용하는 방법을 학습하고 적용하는 과정에서 삽질을 많이 했고, 새롭게 알게 된 것들이 많습니다.

그래서 제가 오해하고 있던 부분들과 해결 과정을 정리해서 남겨보려고 합니다.

 

프로젝트에 FCM 설정하기 & 예제 프로젝트

가장 먼저 해야 할 일은 Firebase 프로젝트를 세팅하는 작업과, 각자의 프로젝트에서 FCM을 사용할 수 있도록 연동하는 작업입니다.

조금만 검색해 봐도 자세하게 잘 정리한 다른 블로그가 많이 있어서 설명을 생략할까 했는데, 뒤에서 설명할 일련의 삽질 과정들을 설명하기 위해서는 세팅 방법도 한번 더 짚고 넘어가야 할 필요가 있어 보였습니다. 그런데 세팅 방법은 이 글에서 바로 설명하기에는 글이 길어질 것 같고, 실제 동작하는 프로젝트 코드와 함께 설명하는 것이 좋다고 생각했습니다.

 

그래서 Github 저장소를 따로 만들었고 Readme에 세팅 방법을 정리해 놓았으니, 참고하시고 도움이 되기를 바랍니다.

https://github.com/zooxop/remote-push-example

 

GitHub - zooxop/remote-push-example: Remote Push Notification(FCM) iOS & macOS 예제

Remote Push Notification(FCM) iOS & macOS 예제. Contribute to zooxop/remote-push-example development by creating an account on GitHub.

github.com

 

 

코드 (AppDelegate) 설명

위의 예제 프로젝트는 SwiftUI 기반으로 작성하였기 때문에 AppDelegate가 처음부터 생성되어 있지 않습니다.

그래서 AppDelegate class를 직접 만들고, 그 안에 알람 권한 요청과 FCM 토큰 발급 처리, 이벤트 핸들러 등 최초 설정과 관련된 코드를 추가해 주었습니다.

 

우선 준수해주어야 하는 Protocol부터 살펴봅니다.

 

UIResponder, UIApplicationDelegate 두 가지는 AppDelegate를 SwiftUI 기반 프로젝트에서 구현할 때 채택해주어야 하는 Protocol입니다. 이 글의 주 내용과는 거리가 있으므로, 자세한 설명은 생략하겠습니다.

 

MessagingDelegate Protocol은 FCM 라이브러리를 사용하기 위한 Delegate Protocol입니다. 앱이 처음 실행되어 초기화될 때, FCM 라이브러리 설정도 함께 초기화해주어야 합니다.

UNUserNotificationCenterDelegate푸시 메시지를 핸들링하는 메서드를 구현하기 위한 Delegate Protocol로, AppDelegate 클래스가 푸시 메시지의 동작에 대한 핸들링을 할 수 있도록 해줍니다.

 

 

이제 didFinishLaunchingWithOptions 메서드에서 초기화 작업을 해줍니다.

 

 

다음은 초기화 코드를 통해 APNs 발급이 정상적으로 이루어지면 호출되는 메서드입니다.

APNs 토큰은 FCM 토큰이 아닙니다. 이 예제에서는 APNs 토큰을 먼저 발급받고, 그 토큰을 FCM SDK로 전달하는 방식으로 동작합니다.

아래의 Messaging.messaging().apnsToken = deviceToken 라인을 확인해 주세요.

FCM SDK가 APNs 토큰을 전달받는 시점에, FCM의 토큰 발급이 시작됩니다.

 

 

정상적으로 FCM 토큰이 발급되면 호출되는 메서드입니다.

이제 이 디바이스가 FCM을 수신할 기본적인 준비를 마쳤습니다.

서버에서 전송하는 푸시 메시지에 이 토큰을 포함시키면, 이 디바이스가 메시지를 수신할 것입니다.

 

 

 

위의 초기화 과정 중 앱이 APNs 등록에 실패하거나, Remote Notification을 위한 구성에 실패한 경우 호출되는 에러 핸들러입니다.

 

 

 

그리고 여기서부터는 메시지가 수신되었을 때에 대한 이벤트 핸들러들입니다.

 

가장 먼저, 사용자가 푸시 메시지를 터치했을 때 호출되는 didReceive 핸들러입니다.

예시로, 메시지를 터치하면 앱의 특정 화면으로 바로 가게 해주는 기능을 구현할 때 쓰일 수 있겠습니다.

 

 

 

앱이 Foreground 상태일 때 호출되는 willPresent 핸들러입니다.

사용자가 우리의 앱을 사용하면서 무언가를 하는 도중에 푸시 메시지를 수신하면 이 핸들러가 호출됩니다.

앱을 사용중일 때는 바로 위의 didReceive 핸들러는 (사용자가 메시지를 터치하지 않는다면) 호출되지 않으므로, 종류별 메시지의 의도에 대한 케이스 처리를 잘해줘야 자연스러운 사용자 경험을 제공할 수 있을 것입니다.

 

 

 

마지막으로 didReceiveRemoteNotification 핸들러입니다.

이 핸들러는 Silent 푸시 메시지를 수신했을 때 호출됩니다.

Silent 푸시는 말 그대로 사용자가 모르게 조용히 처리되는 푸시 알람을 말합니다.

그래서 Background 상태에서도 동작합니다. (대신 누락될 가능성이 있고, 너무 무거운 작업은 하지 않는 것이 좋다고 합니다.)

따라서 사용자가 푸시 알람을 터치하지 않아도 반드시 처리되어야 하는 로직이나 상태값 변경 등의 작업유용할 수 있겠습니다.

 

예시로, 요즘 배달 앱들을 보면 음식 주문 후 가게에서 주문을 승인했다는 정보를 앱 화면에서 실시간으로 확인할 수 있습니다. 서버에서 변경사항을 Silent 푸시로 앱에 전송하면, 앱은 사용자의 인터랙션 없이도 화면의 내용을(상태값을) 실시간으로 변경시킬 수 있습니다.

배달 앱들이 정말로 이 기능을 푸시 서비스를 사용해서 구현한 건지는 알 수 없지만, 이 핸들러를 활용하면 그 기능을 구현할 수 있겠다 싶습니다.

 

 

iOS에서 동작 방식과 macOS 에서의 동작 방식에 약간 차이가 있다는 특이 사항이 있습니다.

iOS에서는 이 핸들러는 반드시 Silent 푸시일 때만 동작합니다. (앱이 Background 상태인지 Foreground 상태인지는 상관이 없습니다.) 그런데, macOS 에서는 그냥 무조건 실행됩니다. Silent여도, Silent가 아니어도 동작합니다.

 

왜 차이가 있는지 나름대로 추측을 해보자면, iOS는 반드시 동시에 하나의 앱만이 Foreground 상태로 존재할 수 있습니다. (iPad의 멀티 태스킹은 논외로 하겠습니다.) 그런데 macOS 환경에서는 그게 아닐 수 있습니다.

정확히는 "사용자는 그렇게 느끼지 않을 수" 있습니다. 지금 이 글을 작성하는 저만 해도, 하나의 모니터에 웹 브라우저와 카카오톡, Xcode 등 여러 개의 앱을 겹쳐서 실행시켜놓고 있습니다. 포커스는 웹 브라우저에 가있다고 할지라도, 카카오톡 화면도 엄연히 모니터에 표시되어 있습니다. 아마도 애플은 이런 사용자의 경험적인 측면을 고려해서 API를 설계한 게 아닐까 싶습니다.

 

그리고 주의점은 시뮬레이터 환경에서는 사일런트 푸시를 수신할 수 없다는 점이고, 백그라운드 상태에서 메시지를 수신하는 경우에는 너무 짧은 시간 안에 많은 양이 수신되면 메시지가 누락될 수 있다는 애플 공식 가이드의 언급이 있으니 적절하게 활용하는 것이 중요합니다. 

그리고 바로 위에서도 언급했지만, 너무 무거운(오래 걸리는) 작업은 하지 않는 것을 권장합니다. 백그라운드에서 앱을 아주 잠깐 깨우면서 실행시키는 것이기 때문에, 로직이 실행되는 도중에 앱이 다시 수면 상태로 돌아가버릴 가능성이 크다고 합니다.

 

- 참고 : Developer document - Pushing background updates to your App

 

Pushing background updates to your App | Apple Developer Documentation

Deliver notifications that wake your app and update it in the background.

developer.apple.com

 

Apple Developer Document

 

 

마주했던 문제와 해결 과정

제가 Remote Push Notification을 통해 구현하고자 했던 기능은 "사용자가 손대지 않고도 화면이 갱신되도록 하는" 기능이었습니다. 위에서 예시를 들었던 배달 앱의 실시간 현황 갱신 기능이 정확히 제가 원하던 것이었습니다.

 

결과적으로는 사일런트 푸시를 이용해서 구현해 낼 수 있었습니다.

앱이 변경시켜야 할 정보를 사일런트 푸시의 메시지로 전달받은 뒤, 앱 내부의 상태값을 알맞게 변경시키도록 했습니다.

 

이때 제가 겪었던 문제는, 사일런트 푸시를 수신하는 메서드가 동작하지 않는 현상이었습니다. 바로 위에서 설명했던 didReceiveRemoteNotification 메서드를 말하는 건데요. 나머지 두 메서드는 정상적으로 동작하는데 이 메서드만 먹통이길래, 처음에는 푸시를 발송하는 Request body의 옵션값을 잘못 주고 있을 거라고 의심했습니다.

하지만 원인은 Xcode 프로젝트의 FCM 관련 설정값에 있었습니다.

 

FCM 세팅 과정 중 Info.plist 파일에 FirebaseAppDelegateProxyEnabled 항목을 추가해주어야 한다는 내용이 있습니다. 앱의 Remote Notification Receiver 메서드를 스위즐링하는 옵션을 OFF 시켜주는 값이라고 합니다. 이 메서드를 스위즐링하는것을 OFF 해주지 않으면, Release 스킴에서는 Remote 푸시를 수신할 수 없다고 합니다. (직접 테스트는 안 해봤습니다.)

 

다음은 해당 내용에 관한 FCM 문서 내용입니다.


FCM SDK는 FCM 등록 토큰에 APN 토큰을 매핑하고 다운스트림 메시지 콜백 처리 중에 애널리틱스 데이터를 캡처하는 등 두 주요 영역에서 메서드를 재구성합니다.

재구성을 사용하지 않으려는 개발자는 앱의 Info.plist 파일에 FirebaseAppDelegateProxyEnabled 플래그를 추가하고 NO(불리언 값) 로 설정하여 재구성을 사용 중지할 수 있습니다. 이 가이드의 관련 영역에서는 메서드 재구성을 사용할 때와 그렇지 않을 때의 코드 예시를 모두 제공합니다.

 

Info.plist -> FirebaseAppDelegateProxyEnabled

 

그런데, FCM 연동 방법을 설명하는 매우 많은 블로그들이 FCM의 SDK에 버그가 있다며 해당값을 String으로 설정해야 한다고 말합니다. 그래서 저도 처음에는 그 블로그들의 글을 따라서 해당 값을 String으로 설정해 놓았었는데요, 오히려 저는 이게 문제였습니다.

FCM 세팅을 완료하고 처음 프로젝트를 실행했을 때부터, Xcode의 콘솔에 아래와 같은 경고가 표시되었습니다.

(이 메시지를 무시하고 넘어갔던 것이 결국 5시간 넘게 삽질을 하도록 만들었습니다 ....)

FIRMessaging Remote Notifications proxy enabled, will swizzle remote notification receiver handlers. If you'd prefer to manually integrate Firebase Messaging, add "FirebaseAppDelegateProxyEnabled" to your Info.plist, and set it to NO. Follow the instructions at:
https://firebase.google.com/docs/cloud-messaging/ios/client#method_swizzling_in_firebase_messaging
to ensure proper integration.

 

콘솔에 "will swizzle remote notification receiver handlers" 라는 경고가 출력되고 있습니다.

FirebaseAppDelegateProxyEnabled 옵션을 NO 로 설정했기 때문에 스위즐이 안되어야 하는 건데, 대놓고 스위즐 될 거라고 경고를 주고 있었습니다. 이걸 너무 늦게 본 거죠. 아무튼 Info.plist에 값을 잘 설정해 주었는데 왜 인식을 못하는지 찾아보기 위해서 공식 문서를 다시 정독했고, 해당 값을 Boolean으로 설정하라는 내용을 찾을 수 있었습니다.

 

최종적으로

- FirebaseAppDelegateProxyEnabled 값의 타입을 Boolean으로 변경했더니 경고는 사라졌고, 

- 사일런트 푸시를 발송하면 didReceiveRemoteNotification 메서드가 잘 작동하게 되었습니다.

 

 

정리

FirebaseAppDelegateProxyEnabled 값을 반드시 Boolean으로 설정해 주어야만 정상적으로 동작합니다.

오히려 String으로 설정해 주었을 때 문제가 되었습니다.

 

그런데 FirebaseAppDelegateProxyEnabled 값을 String으로 설정해주어야 한다고 공통적으로 설명하는 글이 정말 많습니다.

그렇게 설명하는 글이 정말 많은 걸로 보아, 이전에는 String으로 설정해주지 않으면 안 되긴 했었을 것 같습니다.

제 생각에는 과거에 FCM SDK에 버그가 있었던 게 아닐까 싶습니다. 그리고 지금은 SDK가 고쳐진 거겠죠..?!

아무튼, 지금은 Boolean으로 설정해주어야 합니다.

 

그리고 FCM 설정 방법과 프로젝트 전체 코드를 맨 위에 달아놓은 Github 저장소에 업로드해놓았습니다.

최대한 자세하게 설명하려고 노력했으니, 이 글을 보시는 분들이 도움을 얻어가실 수 있기를 바랍니다.