React-native에서 fcm 알림을 받아보고, 추후 이벤트 처리까지 해보도록 합시다.
# Install the ap module
npm i @react-native-firebase/app
# Install the messaging module
npm i @react-native-firebase/message
먼저, FCM을 수신할 수 있는 라이브러리를 설치해줍니다.
Cloud Messaging | React Native Firebase
나머지는 공식문서를 그대로 따라하면 됩니다. 공식문서가 제일 친절하게 설치 방법이 나와있습니다. 다른 블로그 글들을 참조하기 보단, 공식문서를 보는 습관을 들여 봅시다 :)
export const useFcmToken = (): [string, React.Dispatch<React.SetStateAction<string>>] => {
const [fcmToken, setFcmToken] = useState<string>('');
useEffect(() => {
(async () => {
const tempFcmToken = await messaging().getToken();
if (tempFcmToken) {
setFcmToken(tempFcmToken);
}
})();
}, []);
return [fcmToken, setFcmToken];
};
먼저 서버에서 우리의 FCM 토큰을 가지고 있어야 우리의 핸드폰에 FCM 메세지를 보낼 수 있게됩니다.
때문에 해당 hook으로 FCM 토큰을 가지고 온 다음, refresh 요청이나 로그인 요청을 보낼때 받아오기로 합시다.
이제 저희는 어떻게 메세지를 수신할지에 대한 이벤트 콜백 함수를 작성해야 합니다. 이때, 어플리케이션이 foreground
, background
, quit
인 상태들을 구분하여 콜백 핸들러를 각각 작성해주어야 합니다.
import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
function App() {
// Foreground messaging 수신
useEffect(() => {
const unsubscribe = messaging().onMessage(async remoteMessage => {
Alert.alert('A new FCM message arrived!', JSON.stringify(remoteMessage));
});
return unsubscribe;
}, []);
}
먼저 foreground
상태일때 입니다. foreground
상태일때는 App 최상단에 useEffect 내부에messaging().onMessage
함수를 실행하여 fcm 메세지를 수신할 수 있습니다.
// Background message 수신
messaging().setBackgroundMessageHandler(async remoteMessage => {
Alert.alert('A new FCM message arrived!', JSON.stringify(remoteMessage));
});
function App() {
// Foreground messaging 수신
useEffect(() => {
const unsubscribe = messaging().onMessage(async remoteMessage => {
Alert.alert('A new FCM message arrived!', JSON.stringify(remoteMessage));
});
return unsubscribe;
}, []);
}
background
상태일때는 messaging().setBackgroundMessageHandler
를 통해 메세지를 수신해줄 수 있습니다. foreground
에서와 똑같이 처리해 주면 됩니다.
이렇게 처리하고 어플리케이션을 종료하면 알림이 수신되긴합니다. 하지만 이건 backgroundhandler
부분에서 처리한 것이 아니라, foreground
에서 처리한 것입니다.
이는 iOS는 메시지를 받으면 조용하게 앱을 백그라운드 상태로 실행시키기 때문입니다. 이 시점에서 백그라운드 핸들러(setBackgroundMessageHandler를 통해)가 트리거되지만 루트 React 구성 요소도 마운트됩니다. 이때 부작용(예: useEffects, 분석 이벤트/트리거 등)이 앱 내부에서 호출되므로 일부 사용자에게는 문제가 될 수 있습니다.
때문에 어플리케이션을 종료하였을때도 알림이 오면 어플리케이션이 작동되고, foregroundHandler
가 대신 작동해왔었던 것이죠. 또한 알림이 올때마다 루트 React의 구성요소를 마운트 시키는건 비효율 적이므로 우리는 이를 방지해 주어야 합니다.
import App from './App';
import { name as appName } from './app.json';
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';
// Background message 수신
messaging().setBackgroundMessageHandler(async remoteMessage => {
Alert.alert('A new FCM message arrived!', JSON.stringify(remoteMessage));
});
function HeadlessCheck({ isHeadless }) {
if (isHeadless) {
return null;
}
return <App />;
}
AppRegistry.registerComponent(appName, () => HeadlessCheck);
이 문제를 해결하려면 루트 구성 요소에 isHeadless를 삽입하도록 index.js에 Headless 옵션을 넣어줄 수 있습니다. 이제 우리의 어플리케이션이 백그라운드에서 실행되는 경우 이 속성을 사용하여 조건부로 null을 렌더링 하게 됩니다.
여기 까지 진행하였다면, FCM Message를 수신하는데는 문제가 없을 것 입니다. 하지만 이렇게 수신한 메세지를 사용자들에게 노출시키는 것은 별개의 문제입니다.
먼저 FCM 메세지를 수신한대로 사용자에게 노출하는 것은 사용자가 사용하기에도 불편하고, 상황에 따른 처리도 불가능하다, 또한 background
환경에서는 push message를 수신했을 때 자동으로 알림센터에 푸시메시지를 표시해주지만, foreground
환경에서는 push message가 수신되었더라도 알림센터에 표시해주지 않기 때문에 사용자에게 메시지를 노출하기 위해서는 별도의 핸들링을 설정해줄 필요가 있습니다.
npm i @notifee/react-native
이를 위해 우리는 notifee
라이브러리를 설치해주어야 합니다.
import notifee, { EventDetail } from '@notifee/react-native';
export const messageHandler = (message: FirebaseMessagingTypes.RemoteMessage) => {
const { title, body, data } = message;
return notifee.displayNotification({
title: title,
body: content,
data: { messageType: messageType, roomId: roomId },
});
}
먼저 수신한 FCM 메세지를 어떻게 이를 notifee
로 보여줄지에 대한 함수를 작성해 줍니다. notifee.displayNotification
을 통해 메세지를 수신할 수 있습니다.
// Background message 수신
messaging().setBackgroundMessageHandler(messageHandler);
useEffect(() => {
const unsubscribe = messaging().onMessage(messageHandler);
return unsubscribe;
}, []);
그리고 위의 핸들러를 FCM 메세지를 수신처리해 주었던 background, foregroung handler의 callback 함수에 넣어줍니다. 이제 저희는 알림을 수신할 수 있게 되었습니다.
useEffect(() => {
// foreground일때 notifee 알림제어
notifee.onForegroundEvent(async ({ type, detail }) => {
if (type === EventType.PRESS) {
await handleNotificationPress(detail);
} else if (type === EventType.DISMISSED) {
await handleNotificationPress(detail);
}
});
// background, quit일떄 알림제어 -> ios는 quit상태거 없어서 필요없지만 더 공부해봄
notifee.onBackgroundEvent(async ({ type, detail }) => {
if (type === EventType.PRESS) {
await handleNotificationPress(detail);
} else if (type === EventType.DISMISSED) {
await handleNotificationPress(detail);
}
});
}, []);
이제는 알림을 터치했을때 특정 행동을 취하도록 만들어 주어야합니다. 해당 부분도 동일하게 background
와 foreground
시의 이벤트를 나누어서 설정해주어야 합니다.
notifee.onForegroundEvent(async ({ type, detail }) => {
if (type === EventType.PRESS) {
await handleNotificationPress(detail);
} else if (type === EventType.DISMISSED) {
await handleNotificationPress(detail);
}
})
onForegroundEvent
과 onBackgroundEvent
는 callback 함수에 두개의 파라미터를 받게되는데, type
은 알림을 터치했는지 무시했는지에 대한 여부, detail
에는 fcm에서 전송했던 알림의 내용들이 전달됩니다.
// notifee 알림을 클릭했을때 handling
export const handleNotificationPress = async (detail: EventDetail) => {
const messageType = detail.notification?.data?.messageType;
const roomId = detail.notification?.data?.roomId;
// 눌렀을때 처리해줄 로직 작성
};
// notifee 알림을 무시했을때 handling
export const handleNotificationDismissed = async (detail: EventDetail) => {
if (detail?.notification?.id) {
await notifee.cancelNotification(detail.notification.id);
await notifee.cancelDisplayedNotification(detail.notification.id);
}
};
눌렀을때의 handleNotificationPress
핸들링 함수, 무시했을때의 handleNotificationDismissed
핸들링 함수를 각각 작성해 줍니다.
notifee.onForegroundEvent(async ({ type, detail }) => {
if (type === EventType.PRESS) {
await handleNotificationPress(detail);
} else if (type === EventType.DISMISSED) {
await handleNotificationDismissed(detail);
}
});
그리고 notifee
의 이벤트 핸들러의 콜백함수에 적용해주도록 해주면 됩니다.
export const handleNotificationPress = async (detail: EventDetail) => {
const messageType = detail.notification?.data?.messageType;
const roomId = detail.notification?.data?.roomId;
// 여기에 어디 화면으로 이동할지에 대한 로직 작성 필요
};
보통의 어플리케이션은 알림을 눌렀을때 어플리케이션의 특정 페이지로 이동하게 됩니다.
우리 또한 notifee
알림을 눌렀을때 어플을 켜주면서 원하는 페이지로 이동시켜 주어야합니다. 이때는 화면이동시 항상 사용해왔던 navigation
대신 deeplink
를 이용해야 합니다.
npx uri-scheme add yourAppName
deeplink
를 이용하기 위한 스키마를 추가하기 위해 위의 명령어를 터미널에 작성하면, 손쉽에 설정이 됩니다.
// Add the header at the top of the file:
#import <React/RCTLinkingManager.h>
// Add this inside `@implementation AppDelegate` above `@end`:
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
또한 들어오는 앱 링크를 핸들링 하려면 appDeletegate.m
프로젝트에 다음 줄을 추가해야 합니다.
하지만 우리의 프로젝트를 이 코드를 추가했더니 build에 실패하는 문제가 발생하였습니다.
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
if([RNKakaoLogins isKakaoTalkLoginUrl:url]) {
return [RNKakaoLogins handleOpenUrl: url];
}
// 해당 라인 추가
return [RCTLinkingManager application:app openURL:url options:options];
return NO;
}
우리는 이미 카카오 로그인을 위한 카카오 sdk를 사용하고 있어 native 부분에 해당 함수가 이미 존재했던 것이죠
저희는 카카오 로그인을 위한 crossplatformkorea/react-native-kakao-login
을 사용하고 있었는데요, 소셜로그인을 위한 deeplink 라이브러리를 사용하고 있다면 이와 같은 문제가 발생할 수 있습니다.
이는 위의 함수에서 주석 밑부분에 있는 라인을 추가해주면 해결할 수 있습니다.
// deeplinkConfig.js
import { Linking } from 'react-native';
import { LinkingOptions } from '@react-navigation/native';
import { LoginStackParamList } from '@type/param/loginStack';
export const linking: LinkingOptions<LoginStackParamList> = {
prefixes: ['appName://'],
config: {
screens: {
MainScreen: 'main',
CreateRoomScreen: 'createRoom',
RoomDetailScreen: 'room/:roomId',
ChatRoomScreen: 'chatRoom/:roomId',
},
},
async getInitialURL() {
const url = await Linking.getInitialURL();
if (url != null) return url;
},
};
그리고 우리는 이제 어디로 이동할지에 대한 config 파일을 작성해주어야 합니다. react-navigation
을 이용할때와 똑같이 말이죠.
루트 폴더에 deeplinkConfig.js
파일을 생성하고, 해당 파일에 deeplink를 이용할 페이지들을 작성해줍니다, 페이지에 param이 필요한 경우 웹과 같이 room/:roomId
해당 방법처럼 작성해주면 됩니다.
function App(): React.JSX.Element {
return (
<NavigationContainer linking={linking} fallback={<LoadingComponent />}>
<Suspense fallback={<LoadingComponent />}>
<QueryClientProvider client={queryClient}>
<AppInner />
</QueryClientProvider>
</Suspense>
</NavigationContainer>
);
}
그리고 App.tsx의 NavigationContainer
의 NavigationContainer안의 linking 안에 넣어주면 주면 이제 deeplink
를 이용할 수 있게 됩니다.
export const handleNotificationPress = async (detail: EventDetail) => {
const messageType = detail.notification?.data?.messageType;
const roomId = detail.notification?.data?.roomId;
// 여기에 deeplink 작성
await Linking.openURL(`modutaxi://room/${roomId}`);
};
그리고 난 다음 notifee의 handler부분에 Linking
할 코드를 넣어줍니다.
이제 알림을 클릭하면 원하는 페이지로 이동할 수 있게 됩니다. 완성 😊
궁금한게 있습니다.
export const messageHandler = (message: FirebaseMessagingTypes.RemoteMessage) => {
}
여기보시면 content라는 변수가 없는데 사용하셨던데 혹시 변수 지정하신걸까요?
보면서 하고 있는데
const {title, body, data} = message;
이러게 하니 RemoteMessage에는 title 속성이 없다고 나오고
content와 roomId 및 messageType 도 찾을수 없다고 나옵니다
그리고 import { LoginStackParamList } from '@type/param/loginStack';
이것도 볼 수 있을까요?