[철학과 개발] #19 도와주고 싶을 때 도와준다!

https://steemitimages.com/640x0/https://ipfs.busy.org/ipfs/QmZLibU2cxuAstLRGr4RQA8KuptYUX875t5uFKb2SA798Q

P & D

Research and Development가 아니라 Philosophy and Development의 P & D 입니다. 개발글에 있어서 새로운 시도를 하고 있습니다. 그저 개발기를 적어 내려가는 것이 아니라, 어떤 철학을 가지고 개발을 해나가고 있는지, 어떤 시도를 했는지, 개발은 어떻게 하는지를 적어 내려갑니다. 이번 시리즈는 도움을 주고 받는 방법 - 이타인클럽의 철학과 개발 과정을 다룰 것입니다.

이전글 - [철학과 개발] #18 전화번호 로그인/인증


철학: 도와주고 싶을 때 도와준다!

도움은 누구나 요청할 수 있고, 누구나 줄 수 있습니다. 하지만 도움을 요청하는 순간도 다르고, 도움을 줄 수 있는 시간도 다릅니다.

남을 도와주고 싶은 마음은 간절히자만, 시간이 없어서 못하는 사람도 있고, 어딘가 가서 도움 주기도 어려운 사람도 있습니다.

또 도움을 요청하고 싶어도 어떻게 해야 하는지 모르는 사람들. 어느 게시판에 도움을 요청하고 하염없이 기다려야 하는 사람들이 있습니다.

이와 같은 도움 주고 받기 문제의 해결은 실시간 도움 주고 받기입니다.

도움을 주고 싶을 때 준다!
도움이 필요한 순간에 요청한다! 그리고 답변 받는다!

도움이 필요한 사람은 필요한 순간에 도움을 요청을 하고, 요청을 수신한 사람들 중에 그 순간에 도움 주기기 가능한 사람은 도움을 주면 됩니다. 매우 간단합니다. 자신이 도움을 줄 수 있는 여건이 안되면, 그냥 패스하면 됩니다. 다른 여유 있는 사람이 도와줄 것입니다.

이것은 우리가 흔히 사용하는 카카오/우버 택시와 같은 방법입니다. 다수의 사용자들에게 도움 요청 메시지를 보내고, 가장 먼저 수락한 사람과 연결되도록 하는 것!

이것이 이번에 개발할 내용입니다.

개발: Cloud Messaging

개념은 참 쉽습니다. 사용자들에게 메시지를 보내고 가장 먼저 수락한 사람과 연결시켜주는 것!

그러나 구현은 쉽지만은 않습니다. 먼저 앱 사용자들에게 메시지를 보내는 것부터 고민이 됩니다. 문자 메시지를 보낼 것이냐, push notification을 보낼 것이냐.. 여기서는 push notification을 보내기로 합니다.

Firebase는 이 과정을 훨씬 쉽게 도와 줍니다. Thanks Firebase!

관련해서 이전에 정리해 둔 내용이 있습니다. Cloud Messaging의 기본 흐름은 다음 그림과 같습니다.
https://miro.medium.com/max/960/0*4KkxAHPva37acRwJ

  • 도움 요청 메시지를 Firestore (Firebase Database)에 저장
  • Functions (Firebase 서버역할)에서 Firestore에 새로운 메시지가 들어오면 메시지 송신 요청 함수 실행
  • 앱 사용자의 Push Token과 함께 메시지 내용을 Cloud Messaging에 전송
  • Cloud Messaging이 알아서 다수의 앱 사용자에게 메시지 전송

Cloud Messaging을 구현하는데 있어서 다음과 같은 Firebase 모듈이 필요합니다.

  • Firestore: 데이터베이스. 요청 내용이나 사용자 정보등을 관리하는 데이터베이스
  • Functions: 서버 없이 서버의 역할을 하는 기능. 일종의 서버라고 봐도 됨.
  • Cloud Messaging: 앱 사용자들에게 push notification을 보냄
  • Storage: 스토리지. 사용자의 아바타, 채팅의 이미지 등을 저장

실시간 도움 요청 push notification을 구현하기 위해서 다음과 같은 과정을 거칩니다.

  1. Firebase Cloud Messaging 셋업
  2. Firebase Functions 구현

Firebase Cloud Messaging 설정 및 테스트

[React-Native 이타인클럽 앱 Firebase Cloud Messaging Setup] (https://steemit.com/firebase/@etainclub/cloud-messaging-setup)
React-Native 이타인클럽 앱 Firebase Cloud Messaging Test

Cloud Messaging 구현

steemit에 내용은 검색이 잘 안되네요 ㅜ.,ㅜ, 미디엄에 올린 글로 링크합니다.

Cloud Functions을 이용한 Push Notification 구현 1/3
Cloud Functions을 이용한 Push Notification 구현 2/3
Cloud Functions을 이용한 Push Notification 구현 3/3

변경, 추가 사항

위 구현에서 변경 사항, 추가 사항을 정리합니다.
먼저 React Native 버전 0.6이상에서는 패키지 link를 수동으로 할 필요가 없습니다. MainActivity.java 내용도 수정할 필요가 없이 자동으로 만들어 집니다. 0.6 버전 이상 사용을 권장합니다.

DB 구조

이전 자료에는 도움 요청 내용 DB 구조가 이랬습니다.

asks -> user id -> messages -> 0 -> "메시지1"
                               1 -> "메시지2"

Document 아이디(id)로 위와 같은 숫자를 사용하는 것은 좋은 DB 구조가 아닙니다. 왜냐하면 복수의 사용자가 동시의 요청을 했을 때 DB 상태가 꼬일 수 있습니다. DB라는 것이 업데이트하고 읽어 오는데 시간이 걸리므로, 실시간 반영이 안됩니다. 그러다 보니 id가 중복되어 내용들이 삭제될 수 있는 매우 위험한 설계입니다!

따라서 document id를 firestore가 자동 생성하게 합니다.
변경된 db 구조는 다음과 같습니다.

cases - 자동 생성 document id - 메시지 collection (채팅 메시지 콜렉션) 
                              보낸 사람 id
                              수락한 사람 id
                              수락 상태
                              기타 정보

Functions

Functions의 소스코드는 다음과 같습니다.
https://github.com/EtainClub/helpus/blob/master/functions/index.js

Functions에서 Firestore에 접근할 필요가 있을 수 있습니다. 이 때는 다음과 같이 초기화 할 때 databaseurl을 추가해야 합니다.

admin.initializeApp({
  credential: admin.credential.applicationDefault(),
  databaseURL: "https://helpus-206eb.firebaseio.com"
});

그러면 다음과 같이 Functions 내부에서 Firestore를 참조할 수 있습니다.

 const senderRef = admin.firestore().collection('users').doc(`${sender}`);

또, Cloud Message를 보낼 때 push notification의 타이틀, 내용에 언어를 설정할 수 있습니다.

const i18next = require('i18next');

// setup language
    i18next.init({
      fallbackLng: language,
      debug: true, 
      resources: {
          ko: {
            "translation": {
              "header": '[helpus] 도움 요청',
              "abuseHeader": '[helpus] 사용 위반',
              "abuseText": "앱 사용을 위반하여 도움 요청을 할 수 없습니다."
            },
          },
          en: {
            "translation": {
              "header": '[helpus] HELP WANTED',
              "abuseHeader": '[helpus] Usage Violation',
              "abuseText": "You cannot request help becaseu you violated the temr of use."
            },
          },
        },
    });

// usage
const payload = {
      notification: {
        title: i18next.t('header'),
        body: docData.message
      },
      data:{
        title: i18next.t('header'),
        body: docData.message,
        senderId: sender,
        caseId: caseId,
      },
    };

메시지 수신 - App.js

push notification의 동작 개념을 보다 자세하게 설명합니다.
push가 오면 앱은 이 내용을 수신합니다. 우리가 구현할 수 있는 부분은 push가 왔을 때, push를 클릭했을 때 어떻게 처리할 것인가 입니다. 즉, push는 디바이스가 알아서 받는 거고, 이걸 사용자가 클릭했을 때 처리하는 부분을 구현해야 합니다. 단, 앱이 foreground인 상태에서 push가 수신되면 이것은 별로도 처리해줘야 합니다. 먼저 이부분을 구현합니다. 여기서는 클릭하면 다른 스크린으로 이동하게 했습니다.

// notification listener (triggered when a particular notification has been received)
    // if the app is foreground, we need to navigate the screen
    const listenerFG = firebase.notifications().onNotification(async notification => {      
      // check sanity: senderId exists?
      if (notification.data.senderId) {
        Alert.alert(
          t('AppScreen.title'),
          t('AppScreen.message'),
          [
            { text: t('no'), style: 'cancel' },
            { text: t('yes'), onPress: () => NavigationService.navigate('Help', {notificationBody: notification}) },
          ],
          { cancelable: true },
        );
      }
    });

다음으로 앱이 background이거나 꺼져 있을 때 push를 수신하고, push notification를 클릭 또는 열어을 때 처리하는 함수를 구현합니다.

// notification opened (listen for notification is clicked/ tapped/ opened in foreground and backgroud)
    // when we open the notification, handle here
    const listenerBG = firebase
      .notifications()
      .onNotificationOpened(notificationOpen => {
        // check sanity: senderId exists?
        if (notificationOpen.notification.data.senderId) {
          // navigate to Help screen
          NavigationService.navigate('Help', {notificationBody: notificationOpen.notification});
        }
      });

마지막으로 앱이 꺼져있을 때 push를 열었을 때 처리하는 함수를 구현합니다.

// listen the notification being opened or clicked when the app is closed
  const listenerForAppClosed = async () => {
    // app closed
    const notificationOpen = await firebase
      .notifications()
      .getInitialNotification();
    if (notificationOpen) {
      // app was opened by a notification
      // get information about the notification that was opened
      const notification = notificationOpen.notification;
      //// ignore the same notification id since the same notification is received again, don't know why.
      // get noti id from storage
      const notiId = await AsyncStorage.getItem('notiId');
      // set noti id to storage
      await AsyncStorage.setItem('notiId', notification.notificationId);
      if (notification.notificationId === notiId) {
        if (__DEV__) console.log('notification id is the same');
      } else {
        if (__DEV__) console.log('navigating to helpscreen...');
        // check sanity: senderId exists?
        if (notification.data.senderId) {
          // navigate to Help screen
          NavigationService.navigate('Help', {notificationBody: notification});
        }
      }
    }
  };

도움 주고 받는 앱 helpus

앱 사용자 중 다음과 같은 도움이 가능하 사용자들이 있습니다.

  • 최면 상담
  • 영어, 일본어 관련 문제
  • 건강 상담, 심리 상담

물론, 저도 명상, 루시드 드림 관련 도움을 줄 수 있습니다.

보다 자세한 내용은 홈페이지를 참고해 주세요.
https://etain.club