본문 바로가기

iOS/PROJECT

[burstcamp] 백엔드 개발자 없이.. Remote PushNotification 도입 1편 - Firebase Functions

이미지 클릭시 burstcamp github로 이동합니다.

 

 

푸시알림 왜 썼나?

burstcamp는 캠퍼들의 블로그 글들을 모아서 보여주는 앱인데,

앱을 들어오지 않고도 글을 추천해서 보여줄 수 있도록 푸시알림을 도입하게 되었습니다.

 

어떤 푸시알림? 로컬에서 보낼까, 서버에서 보낼까

캠퍼들에게 하루에 한 번 12시 16분에 최신 글을 추천해주는 푸시알림을 도입하기로 했기 때문에

로컬에서 보내는 푸시알림은 사용할 수 없었고,

RSS 피드를 긁어오기 위해 Firebase Functions를 사용하고 있었기 때문에

Firebase Functions를 사용하여 푸시알림을 보내기로 하였습니다.

 

앱에 푸시알림을 추가하자

저희는 p8 방식을 사용하여 파이어베이스에 등록하였습니다. 과정은 참조글에 있는 글을 참조하였습니다.

더보기

 

 

FCM Token을 수집하자

Firebase의 Cloud Message를 사용하여 APNS로 푸시알림을 보내려면 기기별로 FCM 토큰이 필요했습니다.

 

FCM 토큰을 Firebase Functions에서 사용해야 했기 때문에 Firestore에 fcmToken 컬렉션을 만들어 토큰들을 저장하기로 했습니다.

컬렉션의 구조는 fcmToken(컬렉션) -- user의 id값(문서) -- fcmToken (필드)

 

처음에는 FCM 토큰을 받는 즉시 처음에 저장하였는데, 회원가입을 하지 않으면 user의 uid를 가져올 수 없었습니다.

그래서 회원가입을 완료하고 uuid를 생성했을 때 fcmToken을 저장하는 로직으로 변경하였습니다.

 

 

FCM Token을 가져오는 방법

https://firebase.google.com/docs/cloud-messaging/ios/client?hl=ko&authuser=0#token-swizzle-disabled 

 

Apple 플랫폼에서 Firebase 클라우드 메시징 클라이언트 앱 설정

Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기 이 페이지는 Cloud Translation API를 통해 번역되었습니

firebase.google.com

 


이제 푸시알림을 보내보자!

 

 

 

Firebase Functions

당연하겠지만 Swift를 사용할 수 없었고.. 😂

VSCode로 node.js를 사용하여 작업하였습니다.

 

팀원분께서 이미 Functions를 빌드해주셨고

Firebase Functions를 사용하여 푸시알림을 보내는 방법은 공식문서를 참조하였습니다.

 

 

https://firebase.google.com/docs/firestore/query-data/get-data?hl=ko#node.js 

 

Cloud Firestore로 데이터 가져오기  |  Firebase

Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기 이 페이지는 Cloud Translation API를 통해 번역되었습니

firebase.google.com

https://firebase.google.com/docs/functions?hl=ko 

 

Firebase용 Cloud Functions

Firebase용 Cloud Functions는 Firebase 기능과 HTTPS 요청에 의해 트리거되는 이벤트에 응답하여 백엔드 코드를 자동으로 실행할 수 있는 서버리스 프레임워크입니다.

firebase.google.com

https://firebase.google.com/docs/cloud-messaging?hl=ko 

 

Firebase 클라우드 메시징

Firebase 클라우드 메시징(FCM)은 비용 없이 안정적으로 메시지를 보낼 수 있는 플랫폼 간 메시징 솔루션입니다.

firebase.google.com

 

Firebase Functions에서 푸시알림을 보내는 과정은 이렇습니다.

  • Firestore - 가장 최근 feed 가져옴
  • Firestore - isPushOn 이 true로 설정되어 있는 User uuid 가져옴 (푸시알림이 On 되어있는 유저)
  • Firestore - user uuid 가지고 FCM 토큰 가져옴
  • FCM 토큰, 최근 피드 가지고 알림 보내기   data: feedUUID

 

 

구현해보자! Firestore에 접근해서 필요한 정보들을 가져오기

node.js 코드 설명은 생략하고..ㅠ

user, feed, fcmToken 컬렉션들은 모두 직접 생성한 컬렉션입니다.

최근의 Feed, 푸시알림을 받을 FCMToken 정보들을 가져왔습니다.

import { getFirestore, Timestamp } from 'firebase-admin/firestore';
import { logger } from 'firebase-functions';

const db = getFirestore()
const userRef = db.collection('user')
const feedRef = db.collection('feed')
const fcmTokenRef = db.collection('fcmToken')

/**
 * @returns 푸시알림이 on 되어있는 유저의 UUID들
 */
export async function getUsersIsPushOnTrue() {
	const users = await userRef
		.where('isPushOn', '==', true)
		.get()

	const userUUIDs = []
	users.forEach((doc) => {
		userUUIDs.push(doc.data()['userUUID'])
	})

	return userUUIDs
}

/**
 * userUUID로 FCM Token을 찾는다.
 * @param {String} userUUID 
 * @returns userUUID에 해당하는 FCM Token
 */
export async function getFCMToken(userUUID) {
	logger.log('유저 아이디' + userUUID);
	return fcmTokenRef
		.doc(userUUID)
		.get()
		.then((doc) => {
			return doc.data()['fcmToken'] 
		})
}

/**
 * userUUID로 User를 찾는다.
 * @param {String} userUUID 
 * @returns user
 */
export async function getUser(userUUID) {
	return userRef
		.doc(userUUID)
		.get()
		.then((doc) => {
			return doc.data()
		})
}

/**
 * @returns 가장 최근의 Feed
 */
export async function getRecentFeed() {
	return await feedRef
		.orderBy('pubDate', 'desc')
		.limit(1)
		.get()
		.then((querySnapshot) => {
			return querySnapshot.docs.at(0).data()
		})
}

 

 

푸시알림 메세지를 생성하자

Firestore에서 가져온 내용을 바탕으로 title에 제목, body에 알림 내용을 넣습니다. (이모지 가능!!)

그리고 푸시알림을 탭하면 피드 디테일 화면으로 이동해줘야하기 때문에 feedUUID 정보를 data 값으로 함께 넣어줍니다.

tokens는 보낼 장치의 fcmToken들을 한번에 넣어 줄 수 있습니다! (공식문서 참조)

/**
* 1. 메세지를 만든다.
* 2. getMessaging().send(message)
* @param {[String]} token 
* @param {Feed} feed 
* @param {User} writer 
*/
function makeMessage(fcmTokens, feed, writer) {
    logger.log('Feed 작성자에오, ', writer)
    const title = feed['title']
    const feedUUID = feed['feedUUID']
    const nickname = writer['nickname']
    const body = title + ' 📣 by ' + nickname

    const message = {
        notification: {
            title: '오늘의 피드를 확인해보세요 💙',
            body: body
        },
        data: { feedUUID: feedUUID },
        tokens: fcmTokens
    }
    return message
}
https://firebase.google.com/docs/cloud-messaging/send-message?hl=ko&authuser=0#send-messages-to-multiple-devices
 

앱 서버 보내기 요청 빌드  |  Firebase 클라우드 메시징

Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기 이 페이지는 Cloud Translation API를 통해 번역되었습니

firebase.google.com

 

 

진짜 진짜 푸시알림을 보내자

firebase-admin/messaging의 getMessaging을 import해서 사용합니다.

https://firebase.google.com/docs/reference/admin/node/firebase-admin.messaging.messaging

 

Messaging class  |  Firebase Admin SDK

 

firebase.google.com

import { logger } from 'firebase-functions/v1';
import { getUsersIsPushOnTrue, getFCMToken, getRecentFeed, getUser } from './firestoreManager.js';
import { getMessaging } from 'firebase-admin/messaging';

/**
 * 1. push알림이 on 되어있는 유저들의 UUID를 가져온다.
 * 2. 가장 최근의 피드를 가져온다.
 * 3. 피드의 작성자를 가져온다.
 * 
 * 4. 유저의 UUID마다 FCM 토큰을 가져온다.
 * 5. Message를 보낸다.
 */
export async function sendNotification() {
    const userUUIDs = await getUsersIsPushOnTrue()
    const recentFeed = await getRecentFeed()
    const writerUUID = recentFeed['writerUUID']
    const writer = await getUser(writerUUID)
    logger.log('작성자UUID에오', writerUUID)
    logger.log('작성자에오', writer)

    const tokens = await Promise.all(
        userUUIDs.map(userUUID => {
        return getFCMToken(userUUID)
    }))

    logger.log('보낼 토큰들을 만들었어요', tokens)

    sendMessage(tokens, recentFeed, writer)
}


/**
* 메세지를 한번에 모든 token에게 보낸다.
* @param {[String]} token 
* @param {Feed} feed 
* @param {User} writer 
*/
export async function sendMessage(tokens, feed, writer) {
    logger.log('실행하는중~~~~~~~')
    const message = makeMessage(tokens, feed, writer)
    const messaging = getMessaging()	// firebase-admin/messaging의 getMessaging()
    messaging.sendMulticast(message)    // https://firebase.google.com/docs/reference/admin/node/firebase-admin.messaging.messaging
    .then((response) => {
        logger.log('어디로 보냈니? tokens: ' + tokens)
        logger.log(response.successCount + '개 전송 성공');
    })    
    .catch((error) => {
        logger.log('실패')
    })
}

 

어떻게 스케줄링 하나?

이제 만든 이 sendMessage 함수를 매일 12시 16분에 보낼 수 있도록 스케줄링을 해야합니다!

간단하게 pubsub.schedule() 메서드를 사용하면 스케줄링할 수 있습니다.

'every data 12:16' 안의 시간 문법은 링크를 참조해주세요

timeZone을 설정하지 않으면 미국 어딘가로 시간이 default 설정되기 때문에 의도하지 않은 시간에 푸시알림이 갈 수 있습니다 ㅋㅅㅋ

아침 6시에 알림이 오더라구요

import { initializeApp, getApps } from 'firebase-admin/app';
import { pubsub, https } from 'firebase-functions';
import { sendNotification } from './service/apnsManager.js'

// Initialize
if ( !getApps().length ) initializeApp()

export const scheduledSendNotification = pubsub.schedule('every day 12:16').timeZone("Asia/Seoul")
.onRun(async (context) => {
	sendNotification()
})

 

참조한 링크

https://firebase.google.com/docs/functions/schedule-functions?hl=ko 

 

일정 기능  |  Firebase용 Cloud Functions

Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기 이 페이지는 Cloud Translation API를 통해 번역되었습니

firebase.google.com

 

 

 

디버깅하고 싶어요..

터미널을 열고 function의 shell을 실행하고 export한 scheduledSendNotification() 함수를 호출하면

logger로 적은 log들을 볼 수 있습니다.

네. 일일이 중간중간에 어떤 값이 들어갔는지 보는게 속편하더라구요.

 

아니면 Firebase Functions 로그 보기를 활용할 수도있습니다.

로그가 보이는건 똑같습니다.

 

 

배!포!

firebase deploy로 index.js에 있는 함수를 배포합니다.

 

그럼 Functions 탭에서 등록된 함수를 볼 수 있습니다

every day 12:16 !!

 

결론 : 서버개발자 원츄

 

 

다음은 앱에서 푸시알림을 받아서 처리하는 과정입니다.

https://luen.tistory.com/212

 

[burstcamp] Remote PushNotification 도입 2편 - 어디서나 푸시알림 받아서 디테일 화면 띄우기

https://luen.tistory.com/211 [burstcamp] 백엔드 개발자 없이.. Remote PushNotification 도입 1편 - Firebase Functions 푸시알림 왜 썼나? burstcamp는 캠퍼들의 블로그 글들을 모아서 보여주는 앱인데, 앱을 들어오지 않

luen.tistory.com