IOS FCM Background Notification
이번 포스트에서는 ReactNative IOS 환경에서의 FCM Background Notification
을 다뤄보려고 합니다.
아마 이 포스트까지 찾아오신 분들은 공식문서 기반으로 Setting을 진행하였는데 Background Message에 대해서 사용을 할 수 없으셔서 들어오셨을 것이라 예상됩니다. 😂
기본적인 FCM Message
아래 공식문서를 기반으로 Firebase Setting
을 해준다면
기본적으로 ForeGround Message를 받는 것 자체는 어렵지 않습니다.
Cloud Messaging | React Native Firebase
단 iOS의 경우에는 개발자 계정과
APN 인증 키를 Firebase에 등록하는 절차가 필요합니다.
이 또한 공식문서를 잘 따라 한다면 시뮬레이터에서도 알림이 오는 것을 확인할 수 있습니다.
iOS Messaging Setup | React Native Firebase
단 Notifications의 경우 이와 같은 제약 조건이 존재합니다.
- 만약 FCM 메시지를 보낼 때 Data만 보내면 아무런 데이터도 받아볼 수 없으니
이 부분에 대해서 주의하면서 알림을 테스트해보면 됩니다.
부가적인 FCM Test
매번 FCM 메시지를 Postman이나 사설 서버환경을 만들어서 테스트하는 것은 어렵습니다.
이에 대해서 웹 사이트가 존재하니 활용해 봐도 좋습니다.
FCM Tester | Test Push Notification Online
- 여기에서 Server Key의 경우 자신의 Firebase Project에서 확인할 수 있습니다.
- 프로젝트 설정
- 클라우드 메시징
문제의 Background Notification
일단 문제는 저 Background Notification을 받았을 때
이를 기반으로 다른 행동을 하려고 하여 어떤 방법을 써 보아도 이 메시지에 대해서 캐치를 할 수 없다는 점입니다.
공식문서에는 단순히 아래와 같이 애플리케이션 외부에서 다음의 코드를 사용하여 메시지를 받을 수 있다고 말합니다.
// index.js
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import App from './App';
// Register background handler
messaging().setBackgroundMessageHandler(async remoteMessage => {
console.log('Message handled in the background!', remoteMessage);
});
AppRegistry.registerComponent('app', () => App);
하지만 이는 단순하게 Android에서만 동작합니다.
그럼 아래에 나와있는 Headless 문제일까?
아쉽게도 제 경우에는 그렇지 않았습니다.
문제는 iOS native 코드
자체에서 백그라운드 알림을 받았을 때 아무런 행동도 해주지 않고 있기 때문입니다.
또 이를 위한 라이브러리가 존재합니다.
https://github.com/zo0r/react-native-push-notification
설치 방법은 다음과 같습니다.
1. react-native-push-notification 설치
yarn add react-native-push-notification
2. ios 전용 라이브러리 설치
yarn add @react-native-community/push-notification-ios
3. AppDelegate.h(ios 폴더)
#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>
#import <Expo/Expo.h>
#import <Firebase.h>
#import <UserNotifications/UNUserNotificationCenter.h> // header import 추가
@interface AppDelegate : EXAppDelegateWrapper<UNUserNotificationCenterDelegate> // UNUserNotificationCenterDelegate 추가
@end
4. AppDelegate.mm(ios 폴더)
- 공식문서의 다른 부분들은 이미 firebase message를 설치하며 기본적으로 setting이 되어 있으므로
이와 같이 추가만 해주셔도 됩니다.
// 1. 맨 위에 header import 추가
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>
// 2. didFinishLaunchingWithOptions에 다음 내용 추가
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.moduleName = @"main";
// You can add your custom initial props in the dictionary below.
// They will be passed down to the ViewController used by React Native.
self.initialProps = @{};
[FIRApp configure];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; // 추가하기
center.delegate = self; // 추가하기
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
// 3. 맨 마지막에 다음 내용 추가
// Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
[RNCPushNotificationIOS didReceiveNotificationResponse:response];
}
//Called when a notification is delivered to a foreground app.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}
5. pod install
cd ios && pod install --repo-update && cd ..
6. notification 코드 setting
// NOTE : Background 상태에서 알림을 받을 때
const onRemoteNotification = (notification: PushNotification) => {
setNotification({
title: notification.getTitle() || "",
body: (notification.getAlert() as string) || "",
data: (notification.getData() as unknown as NotificationData) || {
data: {
title: "",
body: ""
}
}
});
const result = PushNotificationIOS.FetchResult.NoData;
notification.finish(result);
};
useLayoutEffect(() => {
const type = "localNotification";
PushNotificationIOS.addEventListener(type, onRemoteNotification);
return () => {
PushNotificationIOS.removeEventListener(type);
};
}, []);
이제 이렇게 하게 되면 iOS에서도 Background에서 받은 알림을 확인할 수 있게 됩니다! 🥳
그럼 어떤 점이 다를까?
일단 문제의 근원은 최초에 prebuild시 설정된
application(_:didReceiveRemoteNotification:fetchCompletionHandler:)
이
Foreground Notifications만 처리하기 때문입니다.
application(_:didReceiveRemoteNotification:fetchCompletionHandler:) | Apple Developer Documentation
Use this method to process incoming remote notifications for your app. Unlike the application(_:didReceiveRemoteNotification:) method, which is called only when your app is running in the foreground, the system calls this method when your app is running in the foreground or background. In addition, if you enabled the remote notifications background mode, the system launches your app (or wakes it from the suspended state) and puts it in the background state when a remote notification arrives. However, the system does not automatically launch your app if the user has force-quit it. In that situation, the user must relaunch your app or restart the device before the system attempts to launch your app automatically again.
그럼 무얼 써야 하느냐? 타고 들어가 보면 다음이 나옵니다.
userNotificationCenter(_:willPresent:withCompletionHandler:)
userNotificationCenter(_:willPresent:withCompletionHandler:) | Apple Developer Documentation
우리가 최초에 prebuild 전환을 통해 생성된 native ios code를 보면
// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
이외에는 userNotificationCenter
에 대해 선언이 빠져있기 때문에 이를 설정해 줘서
native → React Native로 Background mode에서 받은 알림에 대해서 전달을 해줘야 합니다.
실제로 이를 선언하고 header 파일과 AppDelegate.mm
을 약간 수정해 준다면 정상적으로 알림의 데이터를 확인해 볼 수 있습니다.
만약 라이브러리 설치에 대해서 의존성을 늘리지 않고 싶거나 직접 구현해 보고 싶은 욕구가 있다면
위의 방법을 참고하여도 좋을 듯합니다.
공식문서와 비교하여 장점?
일단 이렇게 Background Message
를 받게 된다면 장점 아닌 장점? 이 있습니다.
공식 문서 내용을 기반으로 한다면 Background Message
를 받아 보기 위해서는
우리가 주로 사용하는 App
이 AppRegistry
를 통해 등록되기 전에 핸들러를 선언해서 알림을 받아 보아야 합니다.
하지만 이렇게 되면 notification에 대해서 응집도 있게 관리하기 위해 customHook
을 만든다 하더라도
선언된 App 외부에서 핸들러를 선언하게 된다면 단일 customHook 만으로는 목표했던 바를 이루지 못할 것입니다.
// index.js
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import App from './App';
// Register background handler
messaging().setBackgroundMessageHandler(async remoteMessage => {
console.log('Message handled in the background!', remoteMessage);
});
AppRegistry.registerComponent('app', () => App);
하지만 이제 라이브러리나 각자 만든 Native Module를 통해 Message를 받아오게 되면
ForeGround Message와 Background Message에 대해서 하나로 관리할 수 있게 됩니다.
// NOTE : 앱이 실행중일 때 알림을 받았을 때
useEffect(() => {
const unsubscribe = messaging().onMessage(async (remoteMessage) => {
setNotification({
title: remoteMessage.notification?.title || "",
body: remoteMessage.notification?.body || "",
data: (remoteMessage.data as unknown as NotificationData) || {
data: {
title: "",
body: ""
}
}
});
});
return unsubscribe;
}, []);
// NOTE : Background 상태에서 알림을 받을 때
const onRemoteNotification = (notification: PushNotification) => {
setNotification({
title: notification.getTitle() || "",
body: (notification.getAlert() as string) || "",
data: (notification.getData() as unknown as NotificationData) || {
data: {
title: "",
body: ""
}
}
});
const result = PushNotificationIOS.FetchResult.NoData;
notification.finish(result);
};
useLayoutEffect(() => {
const type = "notification";
PushNotificationIOS.addEventListener(type, onRemoteNotification);
return () => {
PushNotificationIOS.removeEventListener(type);
};
}, []);
물론 이 부분에 대해서는 각자 다른 패턴을 이용할 수 있고
알림에 대해서 어떻게 관리할지는 사람마다 다를 수 있기 때문에 장점 아닌 장점?이라고 소개를 하였다.
결론
React Native Firebase의 공식문서의 내용이 조금 부실하지 않은가 싶습니다...
라이브러리가 설치될 때 AppDelegate.mm에 접근 권한이 있어서 메서드를 추가할 수 있다면 Background Message를 받아볼 때 이를 설정해 주거나 이번에 사용한 라이브러리에 대해서 소개를 해주어도 좋을 것 같은데
Github issue에도 보면 ios에서 Background Mode에서 알림을 받을 수 없다는 사람이 몇몇 있는 걸 보면
제가 최초에 설정을 잘못하여 알림을 못 받아본 건 아닐 것 같다는 생각이 들긴 하네요.
추후에 각각의 라이브러리에 대해서 분석해 보고
@react-native-firebase/messaging
공식 문서에 issue를 등록해 보는 것도 좋을 것 같습니다.
그리고 이 예제에서 작성된 코드는 아래의 레포에서 확인해 보실 수 있습니다.
https://github.com/ww8007/rn-background-notification