어서와, 개발은 처음이지?

React Native Firebase 푸시 알림(push notification), background listener - 2.firebase 리스너 구현 본문

React/React Native

React Native Firebase 푸시 알림(push notification), background listener - 2.firebase 리스너 구현

오지고지리고알파고포켓몬고 2019. 4. 9. 01:25



이 글은

React Native Firebase 푸시 알림(push notification), background listener - 1.Android 세팅

React Native Firebase 푸시 알림(push notification), background listener - 2.firebase 리스너 구현(현재글)

React Native Firebase 푸시 알림(push notification), background listener - 3.서버 구현로 구성되어 있습니다.




지난 글에 이어, 앱에서 notification을 receive할 수 있도록 코드를 구현해보겠습니다.

기타 UI가 필요하지 않으므로 React Native의 기본 파일인 App.js에 구현하도록 합니다.


1. Permission 및 Token 확인


우선 아래의 코드를 FirebaseTest의 App.js에 적용합니다.

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import firebase from 'react-native-firebase';

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
  android:
    'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

type Props = {};
export default class App extends Component<Props> {

  async componentDidMount(){
    this._checkPermission();
  }

  componentWillUnmount(){

  }

  async _checkPermission(){
    const enabled = await firebase.messaging().hasPermission();
    if (enabled) {
        // user has permissions
        console.log(enabled);
        this._updateTokenToServer();
    } else {
        // user doesn't have permission
        this._requestPermission();
    }
  }

  async _requestPermission(){
    try {
      // User has authorised
      await firebase.messaging().requestPermission();
      await this._updateTokenToServer();
    } catch (error) {
        // User has rejected permissions
        alert("you can't handle push notification");
    }
  }

  async _updateTokenToServer(){
    const fcmToken = await firebase.messaging().getToken();
    console.log(fcmToken);

    const header = {
      method: "POST",
      headers: {
        'Accept':  'application/json',
         'Content-Type': 'application/json',
         'Cache': 'no-cache'
      },
      body: JSON.stringify({
        user_id: "CURRENT_USER_ID",
        firebase_token: fcmToken
      }),
      credentials: 'include',
    };
    const url = "http://YOUR_SERVER_URL";

    // if you want to notification using server,
    // do registry current user token

    // await fetch(url, header);
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>Welcome to React Native!</Text>
        <Text style={styles.instructions}>To get started, edit App.js</Text>
        <Text style={styles.instructions}>{instructions}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});


순차적으로 설명드리자면,


1. 3번 줄에 react-native-firebase를 import합니다.

2. permission이 있는지 체크합니다 (_checkPermission)

3. permission이 있다면 서버에 token 정보를 저장합니다.(_updateTokenToServer)

node단에서 sdk를 사용하여 특정 사용자에게 Firebase Cloud Messaging를 보낼 수 있습니다.

이를 위해서는 특정 사용자의 token을 unique한 정보와 함께 저장해두는 작업이 필요하기 때문에 _updateTokenToServer라는 형태로 함수를 구현해보았습니다.(본 글에서는 서버단을 따로 구현하지 않습니다.)

4. permission이 없다면 permission을 요청합니다(_requestPermission)

5. permission을 수락하지 않으면 alert 알림을 띄웁니다.


디버그 콘솔을 확인하면 아래와 같이 토큰을 확인할 수 있습니다.

몇번을 새로고침해도 같은 토큰이 나오는 모습을 볼 수 있습니다.

다만 아래의 경우에는 토큰이 새로 발급되기 때문에, 주기적으로 사용자 - 토큰을 동기화 시켜줘야 하겠습니다.


  • The app deletes Instance ID
  • The app is restored on a new device
  • The user uninstalls/reinstall the app
  • The user clears app data.





2. Listener 등록


아래의 코드를 App.js에 적용합니다.

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import firebase from 'react-native-firebase';

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
  android:
    'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

type Props = {};
export default class App extends Component<Props> {

  async componentDidMount(){
    this._checkPermission();
    this._listenForNotifications();
  }

  componentWillUnmount(){
    this.notificationListener();
    this.notificationOpenedListener();
  }

  async _checkPermission(){
    const enabled = await firebase.messaging().hasPermission();
    if (enabled) {
        // user has permissions
        console.log(enabled);
        this._updateTokenToServer();
    } else {
        // user doesn't have permission
        this._requestPermission();
    }
  }

  async _requestPermission(){
    try {
      // User has authorised
      await firebase.messaging().requestPermission();
      await this._updateTokenToServer();
    } catch (error) {
        // User has rejected permissions
        alert("you can't handle push notification");
    }
  }

  async _updateTokenToServer(){
    const fcmToken = await firebase.messaging().getToken();
    console.log(fcmToken);

    const header = {
      method: "POST",
      headers: {
        'Accept':  'application/json',
         'Content-Type': 'application/json',
         'Cache': 'no-cache'
      },
      body: JSON.stringify({
        user_id: "CURRENT_USER_ID",
        firebase_token: fcmToken
      }),
      credentials: 'include',
    };
    const url = "http://YOUR_SERVER_URL";

    // if you want to notification using server,
    // do registry current user token

    // await fetch(url, header);
  }



  async _listenForNotifications(){
    // onNotificationDisplayed - ios only

    this.notificationListener = firebase.notifications().onNotification((notification) => {
      console.log('onNotification', notification);
    });

    this.notificationOpenedListener = firebase.notifications().onNotificationOpened((notificationOpen) => {
        console.log('onNotificationOpened', notificationOpen);
    });

    const notificationOpen = await firebase.notifications().getInitialNotification();
    if (notificationOpen) {
        console.log('getInitialNotification', notificationOpen);
    }
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>Welcome to React Native!</Text>
        <Text style={styles.instructions}>To get started, edit App.js</Text>
        <Text style={styles.instructions}>{instructions}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

_listenForNotifications 함수가 추가되었고, 이 함수는 각 리스너를 등록을 실행합니다.


onNotification은 앱이 활성화 된 상태에서 요청되는 push 알림을 처리하게 됩니다.

onNotificationOpened는 앱이 foreground, background에서 실행 중일때, push 알림을 클릭하여 열 때, 해당 push 알림을 처리하게 됩니다.

getInitialNotification은 앱이 종료된 상황에서 push 알림을 클릭하여 열 때, 해당 push 알림을 처리하게 됩니다.


자세한 설명은 이곳을 참조하세요.



3. push 알림 테스트


앱을 재실행 한 뒤에 아래 이미지처럼 Firebase 콘솔에서 push 알림을 보내봅니다.

앱을 켜둔상태에서, 끈 상태에서, 여러 상황을 가지고 직접 테스트해보시길 바랍니다.


아래 상황은 앱을 종료시킨 상태에서 data를 추가한 push 알림을 보냈을 때, 어떻게 나오는지에 대한 상황입니다.

<푸쉬 알림 내용 입력>


<추가 옵션 - 데이터 입력(타겟은 이전 글과 동일)>


<푸쉬 알림 화면>


<notification의 구성>


notification에는 이렇게 data를 포함하여 보낼 수 있습니다.


마찬가지로 실제 서버에서도 sdk를 사용하여 data를 포함하여 알림을 보낼 수 있으며, 이를 응용하면 token을 사용하여 특정 사용자에게 페이스북 댓글 알림 같은 기능(data에 글에 관한 정보를 넣고 notificationOpen에서 이를 처리할 수 있도록)을 구현할 수 있겠습니다.




4. background listener


여기까지는 push 알림과, 알림을 클릭했을 때 이벤트가 처리되도록 리스너를 등록하는 과정이였습니다.


하지만 push를 통해 data가 들어왔을때, 알림을 클릭하지 않고 즉시 요청/처리가 필요한 경우는 아래와 같은 별도의 작업을 필요로 하게됩니다.


우선 App.js에 messageListener를 추가합니다.

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import firebase from 'react-native-firebase';

type Props = {};
export default class App extends Component<Props> {

  async componentDidMount(){
    this._checkPermission();
    this._listenForNotifications();
  }

  componentWillUnmount(){
    this.notificationListener();
    this.notificationOpenedListener();
    this.messageListener();
  }

  async _checkPermission(){
    // 생략
  }

  async _requestPermission(){
    // 생략
  }

  async _updateTokenToServer(){
    // 생략
  }

  async _listenForNotifications(){
    // onNotificationDisplayed - ios only

    this.notificationListener = firebase.notifications().onNotification((notification) => {
      console.log('onNotification', notification);
    });

    this.notificationOpenedListener = firebase.notifications().onNotificationOpened((notificationOpen) => {
        console.log('onNotificationOpened', notificationOpen);
    });

    // background message listener
    this.messageListener = firebase.messaging().onMessage((message) => {
      // Process your message as required
      // This listener is called with the app activated
      console.log(message);
    });

    const notificationOpen = await firebase.notifications().getInitialNotification();
    if (notificationOpen) {
        console.log('getInitialNotification', notificationOpen);
    }
  }

  render() {
    // 생략
  }
}

// 생략

<FirebaseTest/App.js>


중복된 내용은 가독성 때문에 생략했습니다.


_listenForNotifications에서 등록된 messageListener는 위 onNotification와 마찬가지로 앱이 활성화 된 상태일때 message 알림을 처리합니다.


하지만 background에서 message를 핸들링 하기 위해서는 componentWillUnmount의 messageListener를 실행해주어야 하며, background 상황에서 즉시 핸들링하는 로직은 다른곳에 구현하여야합니다.



임의의 위치(혹은 프로젝트 최상위)에 bgMessaging.js라는 파일을 생성하고 아래와 같이 만들어줍니다.

import firebase from 'react-native-firebase';

export default async (message) => {
    // handle your message
    // you can't see this message, because debugger may off when app closed
    // but you can use react native code like fetch, etc ... 
    
    console.log(message); 
    // fetch('http://YOUR_SERVER/api/regist/something')

    return Promise.resolve();
}

<FirebaseTest/bgMessaging.js>


이 async 함수 안에서 message를 핸들링 할 수 있습니다.


당연히 앱이 꺼져있는 상태이기 때문에 console의 내용은 확인할 수 없지만(message는 앱이 켜져있는 상태에서 this.messageListener에서 확인할 수 있습니다.), fetch같은 네트워크 요청은 실제로 동작합니다.


만약, 백그라운드에서 메세지가 왔을때 문자 메세지를 발송하겠다 같은 기능이 있다면 이곳에 작성하시면 됩니다. 



다음은 프로젝트의 index.js를 수정합니다.

/** @format */

import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import bgMessaging from './bgMessaging';

AppRegistry.registerComponent(appName, () => App);
AppRegistry.registerHeadlessTask('RNFirebaseBackgroundMessage', () => bgMessaging);

<FirebaseTest/index.js>


백그라운드 상태의 message는 우선 순위가 존재하는데, headless task를 통하여 우리 앱의 message가 무시되지 않고 핸들링 될 수 있도록 합니다.



마지막으로 MainApplication.java 부분을 확인합니다.

// 생략
  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
//생략

<FirebaseTest/android/app/src/ ... /MainApplication.java>



이제 앱을 재실행하고, 종료할 때 debugger에 특별한 에러가 발생하지 않는다면 모든 준비가 끝났습니다.



참고) notification은 3가지 종류가 있는데, 테스트 상에서 backround에서 즉시 핸들링이 가능한 알림은 notification-only message가 유일했습니다.


  • notification-only messages from FCM
  • notification + data messages from FCM
  • local notifications


5. 마치며


여기까지 React Native Firebase의 listener를 등록하는 방법에 대해 알아봤습니다.


다음 글에서는 Firebase 콘솔로 메세지를 보내지 않고 node단에서 sdk를 사용해 push 알림과, background listner에 message를 보내는 방법에 대해 알아보겠습니다.



6. 참조


React Native Firebase 공식 홈페이지(rnfirebase.io)





2 Comments
  • 프로필사진 lee 2019.06.04 11:02 안녕하세요 좋은글 감사합니다. 궁금한게 하나 있는데요. onNotification 는 앱이 활성화 중에 동작한다고 쓰여 있는데 왜 componentWillUnmount() 시에 리스너를 등록하는지 알 수 있을까요 ?. 활성화시 동작하려면 componentDidMount() 시에 호출되어야 하는게 아닌가 해서요.
  • 프로필사진 오지고지리고알파고포켓몬고 2019.06.25 02:16 신고 오 날카로운 분석이십니다.

    깊이 생각해보진 않았는데, 언마운트에서 실행되는 부분은 var timer = setInterval(...); -> timer()의 모습과 비슷하지 않을까 추측해봅니다.
    (리스너 등록은 did mount에 작성된 코드입니다)

    의견 감사드리며 더 자세히 알아보고 다시 코멘트 남기도록 하겠습니다!
댓글쓰기 폼