문제상황:
FCM (Firebase Cloud Messaging)으로 앱에 기기별 푸시알림을 구현하던중 발생한 오류이다. (해결방법만 보실분들은 밑으로 스크롤)
firestore 데이터의 변화를 감지하여 FCM 알림을 보내는 기능을 구현하기 위해 다음과정을 따랐었다.
FCM 알림 구현과정
1. 기기 token정보를 firestore db에 갱신
특정기기에 푸시알림을 보내기 위해서 firestore의 user 콜렉션에 기기별 token을 저장할 필요가 있다. 따라서 앱 실행시에 다음과 같은 코드를 통해 기기 token정보를 갱신해준다.
· 앱 시작시 실행되는 코드 중 일부(MainActivity.java)
// Get new FCM registration token
String token = task.getResult();
Map<String, Object> update = new HashMap<>();
update.put("FCMtoken", token);
DocumentReference useremailRef= FirebaseFirestore.getInstance().collection("users").document(getUid());
useremailRef
.set(update, SetOptions.merge())
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "DocumentSnapshot successfully updated!");
}
});
2. firebase functions를 사용하기위해 Firebase CLI를 설치해야하므로, node.js와 npm이 설치된 환경에서 다음 커맨드 실행
npm install -g firebase-tools
3. firebase cloud functions 초기설정
- cmd에서 firebase login을 실행하고, 브라우저를 통해 firebase에 로그인
- Firebase 프로젝트 디렉터리로 이동하여 개발중인 프로젝트 선택
- firebase init firestore를 실행하여 프로젝트에 연결된 firestore db와 연결
- firebase init functions를 실행하고 작성언어(javascript,typescript),eslint사용여부등을 체크하여 초기설정을 마무리
4. functions 폴더내의 index.js 작성
"3. firebase cloud functions 초기설정"을 마치면 컴퓨터에 functions폴더가 설치되고, functions 폴더내의 index.js에 작성하려는 코드를 채워야 된다.
우선 functions에서 firebase에 접근하기 위해서는 Firebase Admin SDK을 사용하는데, 이는 파이어베이스 콘솔로 비공개 키를 생성하여 인증할 필요가 있었다.
비공개 키는 json형식이고, 다음방법으로 다운로드 할 수 있다. (링크참고)
파이어베이스 콘솔 -> 프로젝트 설정 -> 서비스 계정 -> Admin SDK 구성 스니펫 -> Node.js선택 -> 새 비공개 키 생성 클릭
https://firebase.google.com/docs/admin/setup?authuser=0
서버에 Firebase Admin SDK 추가 | Firebase Documentation
의견 보내기 서버에 Firebase Admin SDK 추가 Admin SDK는 권한이 있는 환경에서 Firebase와 상호작용하여 다음과 같은 작업을 수행할 수 있는 서버 라이브러리 집합입니다. 전체 관리자 권한으로 실시간
firebase.google.com
다운로드 이후에 다음코드와 같이 admin을 인증하여 초기화해준다.
· index.js중 일부
// The Cloud Functions for Firebase SDK to create Cloud Functions and set up triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access Firestore.
const admin = require('firebase-admin');
let serAccount = require('프로젝트이름.json의 경로 입력')
admin.initializeApp({
credential: admin.credential.cert(serAccount),
});
그리고 백그라운드에서 firestore의 변화를 감지해서 알림을 보내기위해 다음과 같이 index.js코드를 작성하였다.
· index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
let serAccount = require('프로젝트이름.json의 경로 입력')
admin.initializeApp(
{
credential: admin.credential.cert(serAccount),
}
);
const db = admin.firestore();
/**
* Triggers when a user gets a new follower and sends a notification.
*
* Followers add a flag to `/followers/{followedUid}/{followerUid}`.
* Users save their device notification tokens to `/users/{followedUid}/notificationTokens/{notificationToken}`.
*/
exports.sendReplyNotification = functions.region('asia-northeast3').firestore
.document('/buildings/{mBuildingkey}/posts/{mPostkey}/replies/{replyKey}')
.onCreate(async(snap, context) => {
// Get an object representing the document
// e.g. {'name': 'Marie', 'age': 66}
mBuildingkey = context.params.mBuildingkey;
mPostkey=context.params.mPostkey;
mpostUID=[]
mpostTitle=[]
mreplyBody=snap.data().replyBody
mreplyUID=snap.data().replyUID
// functions.firestore.document('/buildings/${mBuildingkey}/posts/${mPostkey}')
await db.collection("buildings").doc(mBuildingkey).collection("posts").doc(mPostkey).get()
.then(async(doc) => {
if(doc.exists){
// console.log(doc.id);
mpostTitle.push(doc.data().postTitle);
mpostUID.push(doc.data().postUID);
console.log("postUID is",doc.data().postUID);
// console.log(doc.data().created);
}
});
//댓쓴이=글쓴이이면 끝냄
if(mreplyUID==mpostUID[0]){
return;
}
//console.log("postUID is",a);
const payload = {
notification: {
title: '댓글알림',
body: `${mpostTitle[0]}글에 댓글이 달렸습니다! "${mreplyBody}"`,
click_action: "FCM_MY_POST_ACTIVITY"
},
data : {
"mBuildingkey" : mBuildingkey,
"mPostkey" : mPostkey,
"click_action" : "PostDetailActivity",
"mpostTitle" : mpostTitle[0],
"mreplyBody" : mreplyBody
}
};
var tokens = [];
// FireStore 에서 데이터 읽어오기
await db.collection('users').doc(mpostUID[0]).get().then((doc) => {
if(doc.exists){
if(doc.data().notificationSettings==false){
return;
}else{
tokens.push(doc.data().FCMtoken);
}
}
});
console.log(tokens);
if (tokens.length > 0 ){
admin.messaging().sendToDevice(tokens, payload)
.then(function(response) {
// See the MessagingDevicesResponse reference documentation for
// the contents of response.
console.log('Successfully sent message:', response);
})
.catch(function(error) {
console.log('Error sending message:', error);
});
}
})
다음 firebase 공식문서와 예제를 참고하여 작성하였다.
https://firebase.google.com/docs/functions/firestore-events?hl=ko
Cloud Firestore 트리거 | Firebase Documentation
의견 보내기 Cloud Firestore 트리거 Cloud Functions를 사용하면 클라이언트 코드를 업데이트하지 않고도 Cloud Firestore의 이벤트를 처리할 수 있습니다. DocumentSnapshot 인터페이스 또는 Admin SDK를 통해 Cloud
firebase.google.com
https://github.com/firebase/functions-samples/blob/main/fcm-notifications/functions/index.js
GitHub - firebase/functions-samples: Collection of sample apps showcasing popular use cases using Cloud Functions for Firebase
Collection of sample apps showcasing popular use cases using Cloud Functions for Firebase - GitHub - firebase/functions-samples: Collection of sample apps showcasing popular use cases using Cloud F...
github.com
5. FCM 서버에 functions 배포
다음 커맨드를 실행하여 FCM 서버에 functions를 배포한다.
firebase deploy --only functions
배포된 functions는 firebase functions 대시보드에서 확인할 수 있다.
6. 구현확인
실제로 구현됬는지 확인하기위해 함수트리거를 실행시키고, firebase functions 대시보드의 로그를 통해 정상작동여부를 판단한다.
참고) fcm메세지를 수신했을 때 알림 페이로드의 data부분을 처리하는 코드가 요구된다면, (가령 메세지를 눌렀을때 인텐트값을 넣어줘야하는 특정 액티비티를 실행시키고 싶다면)
메니페스트에 이를 관리할 클래스를 추가해주고,
· AndroidManifest.xml
<service
android:name=".java.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
FirebaseMessagingService를 상속하는 메세지 관리 클래스를 만들어주면 된다. (밑의 공식문서를 참고하자)
· MyFirebaseMessagingService.java
public class MyFirebaseMessagingService extends FirebaseMessagingService {
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
//메세지받았을때 실행되는 함수
}
}
https://firebase.google.com/docs/cloud-messaging/android/client?authuser=0
Android에서 Firebase 클라우드 메시징 클라이언트 앱 설정 | Firebase Documentation
의견 보내기 Android에서 Firebase 클라우드 메시징 클라이언트 앱 설정 FCM 클라이언트에 Android 4.4 이상을 실행하며 Google Play 스토어 앱도 설치되어 있는 기기 또는 Google API로 Android 4.4를 실행하는 에
firebase.google.com
해결방법:
"3. firebase cloud functions 초기설정"을 마치고 설치된 functions폴더의 package.json을 다음과 같이 수정한다.
· package.json중 일부
"scripts": {
"lint": "eslint ."
},
를 이렇게 수정
"scripts": {
"lint": "eslint"
},
원인분석:
로그를 다시보자. 로그에 나온 eslint는 자바스크립트로 작성된 코드의 오류를 잡아주는 도구로, 배포하려는 모듈의 메타데이터를 담고있는 package.json에 정의되어있다.
· package.json
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"lint": "eslint",
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "16"
},
"main": "index.js",
"dependencies": {
"firebase-admin": "^9.8.0",
"firebase-functions": "^3.14.1"
},
"devDependencies": {
"eslint": "^7.6.0",
"eslint-config-google": "^0.14.0",
"firebase-functions-test": "^0.2.0"
},
"private": true
}
스택오버플로우를 보면 다음사진부분의 문법이 틀렸다고 하는데, 댓글에서 ecmaVersion문제일 수도 있다고 하여 정확한 이유는 더 알아봐야 할 것 같다.
· package.json중 일부
"scripts": {
"lint": "eslint ."
},
참고링크: