기록의 습관화
article thumbnail

IOS FCM Background Notification

이번 포스트에서는 ReactNative IOS 환경에서의 FCM Background Notification을 다뤄보려고 합니다.

아마 이 포스트까지 찾아오신 분들은 공식문서 기반으로 Setting을 진행하였는데 Background Message에 대해서 사용을 할 수 없으셔서 들어오셨을 것이라 예상됩니다. 😂

기본적인 FCM Message

아래 공식문서를 기반으로 Firebase Setting을 해준다면

기본적으로 ForeGround Message를 받는 것 자체는 어렵지 않습니다.

Cloud Messaging | React Native Firebase

 

Cloud Messaging | React Native Firebase

Copyright © 2017-2020 Invertase Limited. Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License. Some partial documentation, under the

rnfirebase.io

단 iOS의 경우에는 개발자 계정과
APN 인증 키를 Firebase에 등록하는 절차가 필요합니다.

이 또한 공식문서를 잘 따라 한다면 시뮬레이터에서도 알림이 오는 것을 확인할 수 있습니다.

iOS Messaging Setup | React Native Firebase

 

iOS Messaging Setup | React Native Firebase

Copyright © 2017-2020 Invertase Limited. Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License. Some partial documentation, under the

rnfirebase.io

단 Notifications의 경우 이와 같은 제약 조건이 존재합니다.

 

 

  • 만약 FCM 메시지를 보낼 때 Data만 보내면 아무런 데이터도 받아볼 수 없으니
    이 부분에 대해서 주의하면서 알림을 테스트해보면 됩니다.

부가적인 FCM Test

매번 FCM 메시지를 Postman이나 사설 서버환경을 만들어서 테스트하는 것은 어렵습니다.

이에 대해서 웹 사이트가 존재하니 활용해 봐도 좋습니다.

FCM Tester | Test Push Notification Online

 

FCM Tester | Test Push Notification Online

 

testfcm.com

FCM Test

  • 여기에서 Server Key의 경우 자신의 Firebase Project에서 확인할 수 있습니다.
  1. 프로젝트 설정
  2. 클라우드 메시징

Sever Key
알림이 잘 도착한 모습

 

 


문제의 Background Notification

Background 알림

 

일단 문제는 저 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

 

GitHub - zo0r/react-native-push-notification: React Native Local and Remote Notifications

React Native Local and Remote Notifications. Contribute to zo0r/react-native-push-notification development by creating an account on GitHub.

github.com

설치 방법은 다음과 같습니다.

 

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에서 받은 알림을 확인할 수 있게 됩니다! 🥳

 

 

background 알림 테스트


그럼 어떤 점이 다를까?

 

일단 문제의 근원은 최초에 prebuild시 설정된

application(_:didReceiveRemoteNotification:fetchCompletionHandler:)

Foreground Notifications만 처리하기 때문입니다.

application(_:didReceiveRemoteNotification:fetchCompletionHandler:) | Apple Developer Documentation

 

application(_:didReceiveRemoteNotification:fetchCompletionHandler:) | Apple Developer Documentation

Tells the app that a remote notification arrived that indicates there is data to be fetched.

developer.apple.com

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

 

userNotificationCenter(_:willPresent:withCompletionHandler:) | Apple Developer Documentation

Asks the delegate how to handle a notification that arrived while the app was running in the foreground.

developer.apple.com

우리가 최초에 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를 받아 보기 위해서는

우리가 주로 사용하는 AppAppRegistry를 통해 등록되기 전에 핸들러를 선언해서 알림을 받아 보아야 합니다.

하지만 이렇게 되면 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

 

GitHub - ww8007/rn-background-notification: react native background notification test

react native background notification test. Contribute to ww8007/rn-background-notification development by creating an account on GitHub.

github.com

 

profile

기록의 습관화

@ww8007

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!