반응형
해당 글에서는 FCM에서 푸시메시지가 중복되어 전송되는 문제를 해결하기 위한 해결방법에 대해 알아봅니다
1) 문제점
💡문제점
- 앱에 있는 상태(foreground)에서는 FCM을 전송하는 경우 정상적으로 한번 메시지가 전송이 되었지만, 아래와 같이 앱을 벗어난 상태(background)에서 똑같은 FCM이 두 개가 전송되는 문제가 발생하였습니다.
1. 소스코드 확인
💡 소스코드 확인
- 아래와 같이 App.tsx 파일이 실행되었을때, 앱에 있는 상태(foreground) 상태에서 메시지를 받기 위해 messaging().onMessage() 메서드를 사용하였고, 앱을 벗어난 상태(background) 상태에서 메시지를 받기 위해 messaging().setBackgroundMessageHandler()를 메서드를 사용하여 구성하였습니다.
상태 별 확인 | 동작상태 |
foreground 상태 푸시메시지 수신 | 정상 동작 |
inactive 상태 푸시메시지 수신 | 정상 동작 |
background 상태 푸시메시지 수신 | 비 정상 동작 : 2개 메시지 전송 |
import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import { useEffect, useRef } from 'react';
const App = () => {
const messageListener = useRef<(() => void) | null>(null);
/**
* FCM 메시지 리스너 등록 관련 처리
*/
useEffect(() => {
// foreground의 리스너가 존재하는지 여부를 체크하고 존재하면 제거합니다.
if (messageListener.current) {
messageListener.current();
}
console.log("[+] FCM 메시지 리스너가 등록되었습니다.!")
// forground 상태일때, FCM 메시지 수신
messageListener.current = messaging().onMessage(async remoteMessage => await onMessageReceived(remoteMessage));
messaging().setBackgroundMessageHandler(async remoteMessage => await onMessageReceived(remoteMessage));
return () => {
console.log("[-] FCM 메시지 리스너가 사라졌습니다!")
messageListener.current?.(); // 옵셔널 체이닝으로 안전하게 실행
}
}, []);
/**
* FCM 메시지 수신 리스너를 등록합니다. (Foreground, Background 상태)
* @param {FirebaseMessagingTypes.RemoteMessage} message
* @return {Promise<void>}
*/
const onMessageReceived = async (message: FirebaseMessagingTypes.RemoteMessage): Promise<void> => {
const { title, body } = message.notification!;
if (!title || !body) {
console.warn('알림에 제목 또는 내용이 없습니다.');
return;
}
console.log(`[+] 알림 수신: ${title} - ${body}`);
// 중복 알림 방지를 위한 고유 ID 생성
const notificationId = message.messageId || Date.now().toString();
// 기존 알림 취소
await notifee.cancelNotification(notificationId);
// 채널 생성
const channelIdConfig = await notifee.createChannel({
id: `important`,
name: 'Important Notifications',
importance: AndroidImportance.HIGH, // 채널 생성시 중요도를 설정해줍니다.
});
console.log(`notificationId :: `, notificationId);
// 디바이스에 알림을 표시합니다.
await notifee.displayNotification({
id: notificationId,
title: title,
body,
android: {
channelId: channelIdConfig,
smallIcon: 'ic_launcher',
importance: AndroidImportance.HIGH,
visibility: AndroidVisibility.PUBLIC,
},
});
}
}
2) 해결 방법
1. 주요 메서드 API 문서 확인
1.1. onMessage 메서드 확인
💡 onMessage 메서드 확인
- FCM 페이로드를 수신할 때마다 콜백 리스너가 RemoteMessage와 함께 호출됩니다.
- 해당 메서드는 앱이 활성화 상태(forground) 일 때만 호출이 됩니다.메시지 수신 중단을 위한 unsubscribe 함수를 반환합니다.
/**
* When any FCM payload is received, the listener callback is called with a `RemoteMessage`.
*
* Returns an unsubscribe function to stop listening for new messages.
*
* #### Example
*
* ```js
* const unsubscribe = firebase.messaging().onMessage(async (remoteMessage) => {
* console.log('FCM Message Data:', remoteMessage.data);
*
* // Update a users messages list using AsyncStorage
* const currentMessages = await AsyncStorage.getItem('messages');
* const messageArray = JSON.parse(currentMessages);
* messageArray.push(remoteMessage.data);
* await AsyncStorage.setItem('messages', JSON.stringify(messageArray));
* });
*
* // Unsubscribe from further message events
* unsubscribe();
* ```
*
* > This subscriber method is only called when the app is active (in the foreground).
*
* @param listener Called with a `RemoteMessage` when a new FCM payload is received from the server.
*/
onMessage(listener: (message: RemoteMessage) => any): () => void;
1.2. setBackgroundMessageHandler 메서드 확인
💡 setBackgroundMessageHandler 메서드 확인
- 앱이 백그라운드 상태이거나 종료된 상태일 때 호출되는 메시지 핸들러 함수를 설정하는 메서드입니다.
- Android에서는 헤드리스 태스크(headless task)가 생성되어 React Native 환경에 접근할 수 있습니다. 이를 통해 로컬 스토리지 업데이트나 네트워크 요청과 같은 작업을 수행할 수 있습니다.
- 이 메서드는 반드시 애플리케이션 라이프사이클 외부에서 호출되어야 합니다. 예를 들어, 애플리케이션 코드의 진입점에서 AppRegistry.registerComponent() 메서드 호출과 함께 사용해야 합니다.
/**
* Set a message handler function which is called when the app is in the background
* or terminated. In Android, a headless task is created, allowing you to access the React Native environment
* to perform tasks such as updating local storage, or sending a network request.
*
* This method must be called **outside** of your application lifecycle, e.g. alongside your
* `AppRegistry.registerComponent()` method call at the the entry point of your application code.
*
*
* #### Example
*
* ```js
* firebase.messaging().setBackgroundMessageHandler(async (remoteMessage) => {
* // Update a users messages list using AsyncStorage
* const currentMessages = await AsyncStorage.getItem('messages');
* const messageArray = JSON.parse(currentMessages);
* messageArray.push(remoteMessage.data);
* await AsyncStorage.setItem('messages', JSON.stringify(messageArray));
* });
* ```
*
*/
setBackgroundMessageHandler(handler: (message: RemoteMessage) => Promise<any>): void;
2. 해결 방안
💡 해결 방안
- 결론적으로 setBackgroundMessageHandler() 메서드의 경우는 애플리케이션 코드의 진입점에서 AppRegistry.registerComponent() 메서드 호출과 함께 사용해야 합니다.
- React Native 앱의 메인 컴포넌트나 useEffect 등 React 컴포넌트 내부가 아닌, 앱의 시작점(entry point)인 index.js 또는 App.js 파일의 최상위 레벨에서 설정해야 합니다.
- 이렇게 하는 이유는 앱이 백그라운드나 종료 상태일 때도 메시지를 안정적으로 처리하기 위해서입니다.
- React 컴포넌트 내부에서 설정하면 컴포넌트가 언마운트될 때 핸들러도 함께 제거될 수 있기 때문입니다.
// index.js
import messaging from '@react-native-firebase/messaging';
import {AppRegistry} from 'react-native';
import App from './App';
// 여기가 앱 라이프사이클 "외부" - 컴포넌트나 훅 내부가 아님
messaging().setBackgroundMessageHandler(async remoteMessage => {
// 백그라운드 메시지 처리
});
AppRegistry.registerComponent('appName', () => App);
3. 해결 적용 코드
💡 해결 적용 코드
- index.js 파일 내에 해당 messaging().setBackgroundMessageHandler()을 구현하였고, 기존의 App.tsx 파일 내에서 구현하였던 부분은 제거를 하였습니다.
/**
* init Register App
* @format
*/
import { AppRegistry } from 'react-native';
import App from './src/App';
import { name as appName } from './app.json';
import messaging from '@react-native-firebase/messaging';
if (__DEV__) {
import('./src/reactotron.config');
}
// 애플리케이션 코드의 진입점
messaging().setBackgroundMessageHandler(async remoteMessage => {
// 백그라운드 메시지 처리
const { title, body } = remoteMessage;
if (!title || !body) {
console.warn('알림에 제목 또는 내용이 없습니다.');
return;
}
console.log(`[+] 알림 수신: ${title} - ${body}`);
console.log("[+] foreground 인 경우")
// 중복 알림 방지를 위한 고유 ID 생성
const notificationId = message.messageId || Date.now().toString();
// 기존 알림 취소
await notifee.cancelNotification(notificationId);
// 채널 생성
const channelIdConfig = await notifee.createChannel({
id: `important-${type}`,
name: 'Important Notifications',
importance: AndroidImportance.HIGH, // 채널 생성시 중요도를 설정해줍니다.
});
console.log(`notificationId :: `, notificationId);
// 디바이스에 알림을 표시합니다.
await notifee.displayNotification({
id: notificationId,
title: title + type,
body,
android: {
channelId: channelIdConfig,
smallIcon: 'ic_launcher',
importance: AndroidImportance.HIGH,
visibility: AndroidVisibility.PUBLIC,
},
});
});
AppRegistry.registerComponent(appName, () => App);
3) 결과 확인
💡 결과 확인
- 아래와 같이 background 상태에서도 정상적으로 메시지가 전달되는것을 확인하였습니다.
오늘도 감사합니다. 😀
반응형