📑 목차

  1. OAuth Playground로 API 테스트
  2. Request Body 포맷
  3. Android 설정
  4. 메시지 click event 핸들링
  5. Trouble Shooting
  6. 딥링크 테스트
  7. 마무리
  8. 참고 자료

앱 개발자라면 FCM 기반의 push 알림을 한 번쯤은 구현해봤거나 들어봤을 것이다. 나 역시 Android Native 프로젝트에서 FCM push 알림을 구현한 경험이 있었다.

이번에는 React Native 프로젝트 환경에서 FCM push 알림을 구현하게 되었는데, 이 과정을 통해 FCM에 대한 이해도를 이전보다 훨씬 깊게 쌓을 수 있었다. 특히 테스트 과정에서 OAuth 2.0 Playground를 활용해 메시지 포맷을 검증하고, Android/iOS 플랫폼별로 다른 알림 핸들링 방식 때문에 발생하는 이슈들을 해결하는 과정을 가졌다.

이 글에서는 React Native에서 FCM push 알림을 구현하면서 겪었던 시행착오와 해결 과정, 그리고 유용했던 도구들에 대해 정리해보려고 한다.


OAuth Playground로 API 테스트

프론트 개발 단계에서 서버의 지원 없이 메시지 포맷을 확인하기 위해 OAuth 2.0 Playground를 사용했다. 자세한 내용은 프로그래밍 좀비님의 FCM Push Notification(HTTP v1) with OAuth 2.0 Playground / Postman / Terminal - Part2 글을 참고하였다.

Step 1

  • API 선택: Firebase Cloud Messaging API v1
  • Scope: https://www.googleapis.com/auth/cloud-platform
  • 어플이 등록된 Firebase Console 계정으로 인증 완료

Step 2

  • Exchange authorization code for tokens 선택

Step 3

  • POST 요청으로 메시지 발송
POST https://fcm.googleapis.com/v1/projects/파이어베이스콘솔에등록된아이디/messages:send

👉 Request Body 입력 후 Send 버튼을 누르면, Response 영역에서 성공/실패 여부 확인 가능


Request Body Format

FCM은 크게 두 가지 방식이 있다.

  • Notification push: FCM 자체에서 알림을 표시
  • Data push: 앱이 직접 핸들링하여 알림 표시

👉 이번 프로젝트에서는 Notification push 방식을 사용했다.

Notification push

서버에서 전송하는 데이터 포맷

{
  // 공통 notification 데이터
  "message": {
    "token": "fcm token 값",
    "notification": {
      "title": "제목",
      "body": "내용",
    },
    // iOS 설정
    "apns": {
      "headers": {
        "apns-priority": "10",
        "apns-expiration": "1604750400"
      },
      "payload": {
        "aps": {
          "mutable-content": 1
        },
      }
    },
    // android 설정
    "android": {
      "priority": "high",
      "ttl": "4500s",
      "notification": {
      	"channel_id": "default_channel" // 프론트에서 사전에 생성한 channel_id 와 일치해야함, 하단 참고
      }
    },
    // 딥링크 데이터 전달
    "data": {
      "deeplink": "딥링크 데이터",
    }
  }
}

🤖 안드로이드에서 수신받는 데이터

{
  "collapseKey": "...",
  "data": {
    "deeplink": "딥링크 데이터"
  },
  "from": "...",
  "messageId": "....",
  "notification": {
    "android": {
      "channelId": "default_channel"
    },
    "body": "제목",
    "title": "내용"
  },
  "originalPriority": 1,
  "priority": 1,
  "sentTime": 1756176060359,
  "ttl": 4500
}

🍎 iOS에서 수신받는 데이터

{
  "data": {
    "deeplink": "딥링크 데이터"
  },
  "from": "...",
  "messageId": "...",
  "mutableContent": true,
  "notification": {
    "body": "제목",
    "title": "내용"
  }
}

* 참고: 플랫폼별 다른 push 방식 사용 시

(iOS는 notification push를, Android는 data push를 받을 경우)

구현 초반에 android는 data push를, iOS는 notification push의 사용을 고려했었다. 최종적으로는 두 환경에서 모두 notification push를 사용하게 됐지만, 당시 포맷은 하단과 같이 구성하였다.

{
  "message": {
    "token": "....",
    "data": {
      "body": "data",
      "title": "data"
    },
    "apns": {
      "headers": {
        "apns-push-type": "alert",
        "apns-priority": "5",
        "apns-expiration": "1604750400",
      },
      "payload": {
        "aps": {
          "alert": {
            "title": "iOS 알림 제목",
            "body": "iOS 알림 내용"
          },
          "mutable-content": 1
        },
        "customKey": "customValue"
      }
    },
    "android": {
      "priority": "high",
      "ttl": "4500s"
    }
  }
}

Android 설정

안드로이드에서 Notification push를 즉시 알림으로 표시하려면:

  1. priority = high
  2. 서버에서 보내 줄 channel_id 와 일치하는 id로 반드시 앱에서 채널 생성해야 함
import notifee, { AndroidImportance } from '@notifee/react-native';

await notifee.createChannel({
  id:'default_channel',
  name: 'Default Channel',
  importance: AndroidImportance.HIGH,
});

메시지 click event 핸들링

App.tsx에서는 앱의 상태에 따라 다음 메소드로 알림의 click event를 처리할 수 있었다.

  1. 앱 완전 종료 상태(terminated)getInitialNotification
  2. Background / ForegroundonNotificationOpenedApp
  3. Data push, Notifee :
    • Foreground → onForegroundEvent
    • Background → onBackgroundEvent

FCM은 서버로부터 받은 notification을 자동으로 생성하여 사용자에게 알림을 보내지만, foreground 상태에서는 생성되지 않는다. 기획 요구사항에 따라 foreground 상태에서 notifee로 push 알림을 생성했다.

click event 처리 방식

push 도착 시점 \ 눌렀을때완전 종료(terminated)백그라운드포그라운드
완전 종료(terminated)getInitialNotificationonNotificationOpenedApponNotificationOpenedApp
백그라운드getInitialNotificationonNotificationOpenedApponNotificationOpenedApp
포그라운드onBackgroundEventonBackgroundEventonForegroundEvent

위 표처럼, Background 상태에서 받은 알림을 Foreground에서 눌렀을 때는 onNotificationOpenedApp이 실행된다. 반면 포그라운드 상태에서 받은 알림은 notifee를 통해 생성되므로 Foreground에서 눌렀을 때는 onForegroundEvent, Background에서 눌렀을 때는 onBackgroundEvent로 이벤트를 처리한다.

코드 예제

  useEffect(() => {
    // 1)[🤖, 🍎 Android, iOS][Notification] Terminated
    getInitialNotification(getMessaging()).then(async (remoteMessage) => {
      if (remoteMessage) {
        console.log('[🤖, 🍎] getInitialNotification',remoteMessage);
        const deeplink = (remoteMessage?.data?.deeplink as string) ?? null;
        handleDeeplinkOpen(deeplink);
      }
    });

    // 2)[🤖, 🍎 Android, iOS][Notification] Background, Foreground
    const unsubOpen = onNotificationOpenedApp(getMessaging(), (remoteMessage) => {
      console.log('[🍎 iOS]onNotificationOpenedApp',remoteMessage);
      const deeplink = (remoteMessage?.data?.deeplink as string) ?? null;
      handleDeeplinkOpen(deeplink);
    });

    // 3)[🤖, 🍎 Android, iOS][Notifee] Foreground
    const unsubNotifeeFg = notifee.onForegroundEvent(async ({ type, detail }) => {
      const { notification, pressAction } = detail;
      
      if (type === EventType.PRESS) {
        console.log('[🤖, 🍎 onForegroundEvent] EventType.PRESS');
        const deeplink = (notification?.data?.deeplink as string) ?? null;
        handleDeeplinkOpen(deeplink);
      }
    });

    // 4)[Notifee] Terminated, Background
    notifee.onBackgroundEvent(async ({ type, detail }) => {
      const { notification, pressAction } = detail;

      if (type === EventType.PRESS) {
        console.log('[🤖onBackgroundEvent] EventType.PRESS');
        const deeplink = (notification?.data?.deeplink as string) ?? null;
        handleDeeplinkOpen(deeplink);
      }
    });

    return () => {
      unsubOpen();
      unsubNotifeeFg();
    };
  }, []);

handleDeeplinkOpen 메소드에서는 Linking.openURL() 을 통해 딥링크 작업을 해주었다.


🎯 Trouble Shooting

Android 내에서 Push data 가 넘어오지 않는 문제

Android 환경에서 push를 눌렀음에도 onNotificationOpenedApp이 실행되지 않았고, getInitialNotification 은 실행되지만 remoteMessage가 넘어오지 않는 이슈가 있었다.

react-native-firebase issues에서 이 문제에 대한 해결법을 찾을 수 있었다.

어플 맨 초기에는 MainActivity만 존재하였지만, 작업 중간에 SplashActivity를 생성하였다. 이 변경으로 어플 최초 진입 시 SplashActivity -> MainActivity 순으로 진행되었고, 사용자가 push 알림을 눌러 어플에 진입 시 push 데이터는 SplashActivity에서 받았지만 MainActivity로 전달되지 않은 채 소멸이 되어 생긴 문제였다.

결론은 SplashActivity에서 Push 데이터를 MainActivity로 전달하도록 수정했다.

class SplashActivity : ReactActivity() { 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
		
        try {
            val intent = Intent(this, MainActivity::class.java)
            val extras = getIntent().extras

            if (extras != null) {
                // this line is critical for Push Notifications to call
                // onNotificationOpenedApp
                intent.putExtras(extras)
            }

            intent.action = getIntent().action
            intent.data = getIntent().data

            startActivity(intent)
            finish()
        } catch (e: Exception) {
            e.printStackTrace()
            finishAffinity()
        }
    }
}
  }
}

[ 참고자료 ]
https://reactnative.dev/docs/integration-with-existing-apps#creating-a-reactactivity


iOS Linking 설정

Linking.openURL('딥링크데이터')가 iOS에서만 작동하지 않는 이슈가 있었다. 원인은 딥링크 처리를 위한 초기 설정을 해주지 않아서 생긴 문제였다.

React Native 공식문서에서 해결 방법을 찾을 수 있었고, AppDelegate.mm에 Linking 관련 코드를 추가해서 해결했다.

#import <React/RCTLinkingManager.h>

- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  return [RCTLinkingManager application:application openURL:url options:options];
}

딥링크 테스트

개발 과정에서 딥링크가 제대로 작동하는지 확인하기 위해 다음 명령어를 사용했다.

npx uri-scheme open myapp://somepath/details --android
npx uri-scheme open myapp://somepath/details --ios

마무리

React Native에서 FCM 적용은 플랫폼별 차이가 크고, Notifee/Firebase 충돌 같은 삽질 포인트가 많았다. 하지만 OAuth Playground로 메시지 포맷을 직접 검증하고, Android/iOS 각각의 특성을 이해한 덕분에 안정적으로 구현할 수 있었다.


📚 참고 자료

공식 문서

구현 가이드

트러블슈팅

0개의 댓글