본문 바로가기
CodeLab/Firebase

Firebase Web 채팅앱 만들기 - Cloud Messaging과 Functions을 이용한 푸시메세지 기능 - Functions를 통한 FCM 발송

by 블리드카가 2017. 12. 4.
728x90





FCM 발송 작업을 클라이언트 코드에서도 할 수 있으나, Firebase Messaging Server API Key가 클라이언트 코드에 포함되는 것은 보안상 좋은 방법이 아니므로 서버를 사용해야합니다.  여기서 처음으로 Firebase Functions 를 이용해볼 것입니다. 

Functions 는 Firebase 서비스들이 동작하면서 발생하는 이벤트를 받아 서버에서 Firebase Admin을 통하여 Firebase 서비스들을 구동시키는 코드를 수행합니다. Functions가 받는 이벤트는 아래와 같습니다.

  •  Reatime Database 트리거 
     - onWrite() - 실시간 데이터베이스에서 데이터가 생성, 폐기 또는 변경될 때 발생
     - onCreate() - 실시간 데이터베이스에서 새 데이터가 생성 시 발생
     - onUpdate() - 실시간 데이터베이스에서 데이터가 업데이트될 시 발생
     - onDelete() - 실시간 데이터베이스에서 데이터가 삭제될 시 발생

  • Authentication 트리거
    - onCreate() - Authentication을 통해 사용자 생성 시 발생
    - onDelete() -  Authentication을 통해 사용자 삭제 시 발생

  • Hosting 트리거 
    - onRequest() - Hosting으로 Http 요청이 있을 시 발생

  • Storage 트리거
    - onChange() - Storge내에 개체들이 생성 수정 및 삭제 시 발생

  • Messaging 트리거
    - onPublish() - Message 

  • Google Analytics 트리거
    - onLog - Google Analytics 정의된 이벤트에 로그가 쌓이면 발생


이러한 이벤트 트리거 중에  Messaging은 Realtime Database 트리거 중 새 매세지 전송 시에 즉 세 메세지 데이터가 생성이 되는 onCreate시에 채팅방 유저들 중에 접속되지 않은 유저들에게만 보내는 코드를 작성해보겠습니다. 

이번에 코드는 index.html아래가 아닌 프로젝트에 functions 폴더 아래에 index.js 를 열어봅니다. 열면 기본적으로 아래의 코드가 있습니다.
const functions require('firebase-functions');

// // Create and Deploy Your First Cloud Functions
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
//  response.send("Hello from Firebase!");
// });

그리고 프로젝트를 시작하면서 firebase-tools를 통하여 프로젝트를 설정할 때 npm 으로 Depency를 설치하겠냐는 질문에 yes를 누르셨을 것입니다. 만약에 설치를 하지 않고 진행하셨다면, 다시 윈도우즈에 커맨드 창 또는 맥이나 리눅스의 터미널을 실행하여 프로젝트 functions 폴더 경로에서 아래의 명령어를 실행합니다.

npm install
그러면 'firebase-functions'와 'firebase-admin' 라이브러리가 설치가 될 것입니다.

index.js 전체 코드는 아래와 같습니다. 기본적으로 작성되어 있는 코드는 지우고 아래의 코드를 입력해주세요.




코드를 한번 살펴보겠습니다. 우선 코드가 ES 6 코드가 포함되어 있습니다. Firebase  Functions는 현재(2017.11.25) node.js 6.11.5 버전으로 서비스가 운용되고 있습니다. 6.11.5 버전의 node.js는 100퍼센트는 아니지만 99퍼센트에 가까운 ES 6 지원을 합니다. 

우선 서비스 사용을 위해 functions와 admin 모듈을 생성하고, admin을 초기화 하는 코드 입니다.
const functions require('firebase-functions');
const admin require('firebase-admin');

node.js는 npm을 통해 설치한 라이브러리를 require라는 전역함수를 통해 불러옵니다. 다른 node 라이브러리를 설치하시고 require 명령어를 통해 불러와 사용할 수 있습니다.

아래의 코드는 firebase-admin 라이브러리를 통해 Firebase의 다른 서비스를 접근할수 있게 합니다. 
admin.initializeApp(functions.config().firebase);


서버가 코드를 작성하는 개발자의 서버가아니라 구글에서 운영하는 서버이기 때문에 키가 필요 없이 위의 코드로 초기화가 됩니다. 만약에 Firebase admin을 자체 서버에서 운영한다면 위 코드는 동작하지 않습니다.  직접 서버를 운영한다면 아래와 같은 코드로 대신하게 됩니다. 
var admin = require("firebase-admin");

var serviceAccount = require("path/to/serviceAccountKey.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
});

여기서 serviceAccountKey.json은 아래의 Firebase Console 화면에서 json파일을 다운로드 받을 수 있습니다.


프로젝트 설정에는 Firebase 서비스를 이용하는데 필요한 각종 API 키들을 확인할 수 있는데 '서비스 계정' 탭으로 들어갑니다.  


서비스 계정탭 화면을 보면 다시 좌측에 'Firebase Admin SDK' 가 선택되어져 있고, 내용이 있는 화면 아래에 '새 비공개 키 생성' 이라는 버튼이 있습니다.  이 버튼을 클릭 하시면 json 파일을 하나 다운로드하게 됩니다.

Firebase Admin을 초기화 하는 코드 아래에는 본격적으로 Realtime Databse의 이벤트를 받아 우리가 원하는 로직을 수행하는 코드가 작성되게 됩니다.

exports.sendNotification functions.database.ref('Messages/{roomId}/{messageId}').onCreate(event => {
     ... (생략)
});
Firebase Functions가 수행하기 바라는 메소드를 exports 해야합니다. export하는 메소드의 명칭은 원하는 명칭으로 정하시면 됩니다. 
Realtime Database경로가 입력이 되어 있는데 {roomId}/{messageId} 어떤 값이 와도 되며, 중괄호 되어 있는 부분은 나중에 이벤트가 발생한 후 콜백함수에서 주어지는 이벤트객체 안에 포함되어 전달되어집니다. 'Messages/{roomId}/{messageId}' 이 위치에 데이터가 새로 생성이 되면 이벤트가 발생합니다.

onCreate 콜백 함수는 ES6 문법인 화살표 함수로 되어 있습니다.  아래와 같다고 보면 됩니다. 화살표 함수와 일반 무명함수를 콜백함수로 두는 것에는 약간의 차이가 있습니다.  화살표 함수는 자신의 고유한 this를 가지지 않습니다.
function(event){
     ... (생략)
}


콜백함수 내부를 살펴보겠습니다. 아래와 같이 RoomUsers 위치에서 메세지 보낸 곳의 방 유저들 리스트를 구하기 위하여 once메소드의 promise 받고, UserConnection 위치에서 connection값이 true 인 값들을 구하기 위한 promise를 받았습니다.

const promiseRoomUserList admin.database().ref(`RoomUsers/${roomId}`).once('value'); // 채팅방 유저리스트
const promiseUsersConnection admin.database().ref('UsersConnection').orderByChild('connection').equalTo(true).once('value');

UserConnection 위치에서 데이터를 구할 때, 필터를 적용하였습니다. 필터는 반드시 정렬을 먼저 수행해야합니다. UserConnection 하위 키 값에서 connection 을 기준으로 정렬을 수행하고, equalTo 메소드로 값이 true 인 값을 필터링 합니다.

그 다음 코드가 아래의 코드 입니다.  Promise.all() 함수가 수행하게 되어 있습니다. Promise.all 함수는 앞서 수행한 두개의 Promise 가 모두 완료가되면. then함수의 첫번째 콜백함수로 반환됩니다. 코드에는 생략되어 있지만 만약 실패하게 되면 then 함수 두번째 파라미터인 콜백함수가 수행됩니다.
return Promise.all([promiseRoomUserListpromiseUsersConnection]).then(results => {
       ... (생략)
 });


두개의 Promise가 모두 정상적으로 동작이 되면  채팅방에 있는 유저들 중에 접속하지 않은 유저들을 골라내는 코드입니다.
        const roomUsersSnapShot = results[0];  
        const usersConnectionSnapShot = results[1]; 
        const arrRoomUserList =[];
        const arrConnectionUserList = [];

        if(roomUsersSnapShot.hasChildren()){
           roomUsersSnapShot.forEach(snapshot => {
               arrRoomUserList.push(snapshot.key);
           })
        }else{
            return console.log('RoomUserlist Data가 없습니다.')
        }

        if(usersConnectionSnapShot.hasChildren()){
            usersConnectionSnapShot.forEach(snapshot => {
                const value  = snapshot.val();
               if(value){
                   arrConnectionUserList.push(snapshot.key);
               }
            })
        }else{
            return console.log('UserConnections Data가 없습니다.');
        }

        const arrTargetUserList arrRoomUserList.filter(item => {
            return arrConnectionUserList.indexOf(item) === -1;
        });


        const roomUsersSnapShot = results[0];  
        const usersConnectionSnapShot = results[1]; 
        const arrRoomUserList =[];
        const arrConnectionUserList = [];

        if(roomUsersSnapShot.hasChildren()){
           roomUsersSnapShot.forEach(snapshot => {
               arrRoomUserList.push(snapshot.key);
           })
        }else{
            return console.log('RoomUserlist is null')
        }

        if(usersConnectionSnapShot.hasChildren()){
            usersConnectionSnapShot.forEach(snapshot => {
                const value  = snapshot.val();
               if(value){
                   arrConnectionUserList.push(snapshot.key);
               }
            })
        }else{
            return console.log('UserConnections Data가 없습니다');
        }

        const arrTargetUserList arrRoomUserList.filter(item => {
            return arrConnectionUserList.indexOf(item) === -1;
        });

      
 

이렇게 추려진 유저들의 FCM Token을 구해 Messaging을 발송하는 코드 입니다. click_action에는 각 자의 앱주소를 입력합니다.

for(let i=0arrTargetUserListLengthi++){
    admin.database().ref(`FcmId/${arrTargetUserList[i]}`).once('value',fcmSnapshot => {
        const token = fcmSnapshot.val();
        if(token){
            //메세지에 포함될 데이터
            const payload = {
                notification: {
                    titlesendUserName,
                    bodysendMsg,
                    click_action :`개인별Firebase앱주소/?roomId=${roomId}`,  //개인별Firebase앱 주소
                    iconsendProfile
                }
            };
      //메세지발송
            admin.messaging().sendToDevice(tokenpayload).then(response => {
                response.results.forEach((result, index) => {
                    const error = result.error;
                    if (error) {
                        console.error('FCM 실패 :'error.code);
                    }else{
                        console.log('FCM 성공');
                    }
                });
            });
        }
    });
}



대상 유저들의 id 값들을 반복문을 실행하여, FCM Token을 검색 한 뒤, 함께 전달할 데이터를 payload에 담고, sendToDevice 메소드로 푸시를 보내게 됩니다.

메세징을 보내는 Functions 코드는 완성되었습니다. 

코드 중간 중간에 console.log가 실행되어 있습니다. 이는 Firebase functions의 디버깅을 위해 제공되어지는 firebase-tools에 포함된 Functions의 shell프로그래밍에서 확인을 하거나, Firebase console 화면에서 확인할 수 있습니다. 

일단 Functions의 shell 프로그래밍에 대하여 잠시 알아보겠습니다. 아래는 shell 구동 명령어 입니다. 윈도우즈의 명령프롬프트나 리눅스 또는 맥의 터미널에서 아래의 명령어를 입력해보세요.

firebase experimental:functions:shell



위 그림처럼 터미널 화면의 firebase 앞에 입력커서가 생깁니다.

예제에서 작성한 sendNotification을 테스트 하려면 아래와 같이 입력합니다.

sendNotification('data', {params : {roomId: 방ID, messageId: 메세지ID}})
입력하면 Functions가 실행되며 로그가 찍히는 것을 볼 수 있습니다. 하지만 아쉽게도 기능이 완벽하지 않아서, Functions에서 데이터베이스 경로를 지정하는 파라미터는 입력할수 있으나.. 데이터를 테스트로 입력하는 파리미터는 없어서 payload에서 title 부분에서 에러가 발생할 것입니다. 테스트를 위해서는 payload 부분에서 할당되는 데이터가  undefined 될 시에는 테스트 데이터가 입력되도록 수정이 필요합니다.

그리고 console.log는 Firebase console 화면에서 아래의 위치에서 확인이 됩니다.



여기까지 FCM 발신 부분 작성이 완성되었습니다. 다음은 FCM 수신을 위해 클라이언트에서 동작하는 서비스 워커를 작성하겠습니다.





챕터 완성 소스  :

13. Cloud Messaging과 Functions을 이용한 푸시메세지 기능 - Functions를 통한 FCM 발송.zip





  1. 예제 소개
  2. Firebase 설정하기
  3. Hosting을 활용한 프로젝트 준비 작업
  4. Authentication을 이용한 유저 가입 및 로그인 구현하기
  5. Realtime Database를 이용한 채팅기능 구현 - Reatime Database 특징 및 데이터 구조
  6. Realtime Database를 이용한 채팅기능 구현 - 유저데이터 저장하기
  7. Realtime Database를 이용한 채팅기능 구현 - 유저리스팅 화면
  8. Realtime Database를 이용한 채팅기능 구현 - 채팅화면 및 채팅메세지 리스팅
  9. Realtime Database를 이용한 채팅기능 구현 - 채팅메세지 전송기능
  10. Realtime Database를 이용한 채팅기능 구현 - 채팅방 리스팅화면
  11. Realtime Database를 이용한 채팅기능 구현 - 채팅방 초대 기능
  12. Realtime Database를 이용한 채팅기능 구현 - 접속 중인 유저 표시하기
  13. Storage를 이용한 파일 전송기능
  14. Cloud Messaging과 Functions을 이용한 푸시메세지 기능 - FCM Token 정보 저장
  15. Cloud Messaging과 Functions을 이용한 푸시메세지 기능 - Functions를 통한 FCM 발송
  16. Cloud Messaging과 Functions을 이용한 푸시메세지 기능 - Service worker를 이용한 FCM수신
  17. Realtime Database 권한 설정

 




728x90