React-Native에 Firebase Cloud Messaging(FCM) v6 도입하기

박재현 ( Jcurver )·2023년 2월 8일
0

FCM 이거 생각보다 어렵네...

Firebase Cloud Messaging(이하 FCM)은 무료로 메시지를 안정적으로 전송할 수 있는 크로스플랫폼 솔루션입니다.

Keepit 프로젝트에서 유저에게 알림을 전송하기 위해서 FCM을 사용하였습니다.

React-Native 전용 FCM docs가 존재해서 쉽게 적용할 수 있으리라 생각했습니다.

그럼에도 불구하고 FCM 적용은 정말 쉽지 않았습니다.

가장 어려웠던 점은 아래 3가지였습니다.

  1. React Native는 Open Source Project이기에 docs업데이트가 느리고 친절하지 않다.
  2. 버전의 호환을 맞춰주어야 하는 것들이 여러개였고, Stack Overflow, Github등에서 나누어지는 이야기도 내가 설정한 버전들과 대부분 달라서 호환이 되지 않는다.(RN 68.2)
  3. dev / prd 서버를 구분해서 토큰과 ci 설정을 해야하는데 관련 레퍼런스가 거의 없다.

처음부터 이런 사실을 알기는 어려웠고, 하나하나 풀어내야 했습니다.

FCM의 작동 원리

먼저 작동 원리부터 살펴보겠습니다.
사진 출처: dev.to

  • FCM에 앱을 등록하고 FCM과 연동한다.
  • 프론트에서 원하는 타이밍에 FCM토큰을 발급받아 백엔드로 보낸다.
  • 백엔드는 토큰을 유저 정보와 연동하여 보관하다가 필요할 때 조회해서 Notification Provider에게 푸시 알림을 요청한다.
    (FCM토큰을 디바이스별로 부여하기 때문에 개별 푸시가 가능하다.)

FCM 구현을 해봅시다

기본적으로 Docs가 조금 불친절해서 docs와 함께 여러 블로그를 함께 보았습니다.
기본적인 구현 방법(apns, firebase 연결)은 docs와 아래 블로그에 잘 게시되어있습니다.

https://honeystorage.tistory.com/306
https://velog.io/@dody_/RN-Library-rn-firebase-cloud-messaging-FCM-%EC%84%B8%ED%8C%85%ED%95%98%EA%B8%B0

이 블로그만 의존해서 잘 작동하는 분들도 많겠지만 대부분 블로그는 RN 59~63버전에 맞춰진 내용들이 많았습니다.

docs와 타 블로그에 설명된 내용 외에 제가 헤맸던 부분 위주로 글을 작성해보고자 합니다.

Podfile 수정하기

제 환경은 다음과 같습니다.

React-Native ver 0.68.2
firebase v6
"@react-native-firebase/analytics": "^16.1.1",
"@react-native-firebase/app": "^16.1.1",
"@react-native-firebase/messaging": "^16.1.1",
ios 11.0
dev / prd 서버 구분
작업환경 : Macbook M1 pro

본인과 팀원의 환경에 호환되도록 podfile을 잘 다듬어주어야합니다.

ios/Pods/Podfile 은 다음과 같습니다.

require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
       
platform :ios, '11.0'
install! 'cocoapods', :deterministic_uuids => false

pod 'Firebase', :modular_headers => true
pod 'FirebaseCore', :modular_headers => true
pod 'FirebaseCoreInternal', :modular_headers => true
pod 'FirebaseMessaging', :modular_headers => true
pod 'GoogleUtilities', :modular_headers => true

post_install do |installer|
  react_native_post_install(installer)
  __apply_Xcode_12_5_M1_post_install_workaround(installer)
  end
  target 'alphaTests' do
    inherit! :complete
  end
  
  def shared_pods
  config = use_native_modules!
  # Flags change depending on the env values.
  flags = get_default_flags()
  use_react_native!(
    :path => config[:reactNativePath],
    # to enable hermes on iOS, change `false` to `true` and then install pods
    :hermes_enabled => flags[:hermes_enabled],
    :fabric_enabled => flags[:fabric_enabled],
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/.."
    )
    
    
    permissions_path = '../node_modules/react-native-permissions/ios'
    pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
    pod 'Permission-PhotoLibrary', :path => "#{permissions_path}/PhotoLibrary"
    pod 'Permission-PhotoLibraryAddOnly', :path => "#{permissions_path}/PhotoLibraryAddOnly"


  use_flipper!({'Flipper' => '~>0.125.0', 'Flipper-Folly' => '~>2.6', 'Flipper-RSocket' => '~>1.4'})
end
target 'alphaDEV' do
  shared_pods
end

target 'alphaPRD' do
  shared_pods

end
  1. dev / prd 환경을 구분지어주기 위해서 alphaDEV와 alphaPRD를 만들고 중복되는 내용은 shared_pods에 담아서 중복을 방지합니다.
  2. use flipper!는 다음과 같이 사용했습니다. use_flipper!({'Flipper' => '~>0.125.0', 'Flipper-Folly' => '~>2.6', 'Flipper-RSocket' => '~>1.4'})
    Flipper는 125~159버전 이내에서 맞추어주면 됩니다.
  3. pod 'Firebase', :modular_headers => true
    pod 'FirebaseCore', :modular_headers => true
    pod 'FirebaseCoreInternal', :modular_headers => true
    pod 'FirebaseMessaging', :modular_headers => true
    pod 'GoogleUtilities', :modular_headers => true
    이것들을 꼭 상단에 추가해주도록 합니다.
  4. M1맥북으로 작업하는 경우 __apply_Xcode_12_5_M1_post_install_workaround(installer) 를 꼭 추가해줍니다.

이 환경을 구성하는데 총 3일이상 꼬박 시행착오를 겪었습니다.ㅜㅜ

IOS xcode 설정

dev / prd 가 각각 정상적으로 운용되기 위해서는 아래와 같은 구조가 되어야합니다.
즉 토큰을 각각 발급받아야 합니다.
(Kps는 Keepit 팀 서버입니다.)

dev/prd의 토큰을 각각 따로 받기 위해서는 추가적인 설정이 필요합니다.

PATH_TO_GOOGLE_PLISTS="${PROJECT_DIR}/alpha"

case "${CONFIGURATION}" in

   "alpha" )
        cp -r "$PATH_TO_GOOGLE_PLISTS/GoogleService-Info.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" ;;

   "alpha copy" )
        cp -r "$PATH_TO_GOOGLE_PLISTS/GoogleService-Info-prod.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" ;;

    *)
        ;;
esac

위 설정을 따로 해주지 않는경우 FCM 관련 plist는 default로 GoogleServise-Info.plist를 바라보게 됩니다. 빌드 페이즈 마지막 부분에서 이 경로를 분기처리 해주어야 합니다.(물론 Firebase 에서 GoogleServise-Info-prod.plist처럼 plist를 rename해서 가져와서 같은 레벨 폴더에 구성시켜야 합니다.)

FCM을 사용하며 주의(고려)해야 할 부분

같은 기기에서 다른 아이디를 쓰는 경우?
-> 로그아웃 시점에 토큰을 삭제해야 한다.

같은 아이디로 여러 기기를 쓰는 경우
-> 이 케이스를 위해서 fcm과 id를 일대일 대응 시키면 안된다. id를 기준으로 알림을 발송해야한다. 또는 마지막 기기를 제외하고 타 기기에서 강제 로그아웃시키는것도 방법이 될 수 있다.

추가적으로, 유저가 이탈한 경우 또는 자주 사용하지 않는 기기가 있을 수도 있습니다. 이런 경우 토큰을 삭제해주거나, JWT와 함께 수명을 관리하는 등의 방법으로 불필요한 리소스 낭비를 막을 수 있습니다.

그래서 프론트 로직에 들어가야 하는건?

  • 로그인 시 등록을 위해 서버로 FCM 토큰 전송
  • 로그아웃 시 삭제를 위해 서버로 FMC 토큰 전송
  • 앱 실행시 로그인 되어있는 경우 서버로 FCM토큰 전송(타임스탬프 갱신이 필요한 경우만)

My Code

import { setFcmToken } from './store/feature/deviceSlice';
import { putMembersFcmToken } from './api/user';
import messaging from '@react-native-firebase/messaging';
import RootNavigation from './RootNavigation';

function CloudMessaging() {
  const [loading, setLoading] = useState(true);

  async function requestUserPermission() {
    const authorizationStatus = await messaging().requestPermission();

    switch (authorizationStatus) {
      case 0:
        checkToken();
        break;
      case 1:
        checkToken();

        messaging().onNotificationOpenedApp((remoteMessage) => {
          if (remoteMessage) {
            RootNavigation.navigate('AlarmMainScreen', {
              backgroundAlarmData: remoteMessage?.data,
            });
          }
        }); // 앱이 백그라운드에 있는 경우

        messaging()
          .getInitialNotification()
          .then((remoteMessage2) => {
            if (remoteMessage2) {
              RootNavigation.navigate('AlarmMainScreen', {
                backgroundAlarmData: remoteMessage2?.data,
              }); 
            }
            setLoading(false);
          }); // 앱이 꺼져있는 경우
        break;
      default:
        break;
    }
  }
  useEffect(() => {
    requestUserPermission();
  }, []);
}
export default CloudMessaging;

백그라운드에 있는 경우와 앱이 꺼진 경우 알림이 올 수 있도록 구성하였고, json으로 데이터를 받아서 Navigate 시켜주었습니다.(인앱 알림은 정책상 제외했습니다.)

메인페이지에서 알림 탭으로 이동시키는데, 알림탭에서도 알림 이벤트가 수신된 경우 이를 한번 더 분기처리하여 화면을 이동하거나 적절한 이벤트를 보여줍니다.
(굳이 2번 이동하는 이유는 Navigation Stack을 관리하기 위함입니다.)

이상으로 마치겠습니다. 읽어주셔서 감사합니다 :)

profile
FE developer / Courage is very important when it comes to anything.

0개의 댓글