- 
          
          [FCM] FCM으로 notification 구현하기 - 3(Firebase Functions로 functions 구현)Android 2025. 9. 25. 09:30728x90반응형firebase functions 초기 셋팅이 끝났다면 functions/index.js 파일에 직접 함수를 작성해주어야한다 나의 경우 두 가지 경우에 알림을 받도록 했다 - 내가 작성한 글에 좋아요가 추가되었을 때 - 내가 작성한 글에 새로운 댓글이 추가되었을 때 나의 경우 fireStore의 post의 document의 필드로 likes를 배열로 만들고 좋아요가 추가되거나 삭제되면 해당 필드에 아이디값이 추가되거나 삭제되게해놓았다 그리고 댓글의 경우 해당 post의 하위 document로 해놓았기 때문에 접근하는 방법이 조금 달랐다  firebase functions에서 제공하는 fireStore 함수 트리거 좋아요가 추가되었을 때는 onDocumentUpdated를 사용해서 업데이트되기 전의 값과 업데이트 후의 값을 비교해서 좋아요의 갯수를 비교했고, 새로운 댓글이 추가된 경우에는 post의 하위 document가 추가되는 것이기 때문에 onDocumentCreated를 사용해서 comment라는 document가 새로 생성될 때(새로운 댓글이 달릴 때) 트리거하도록 했다 만약 생성, 업데이트, 삭제를 다 트리거하고 싶다면 onDocumentWritten을 쓰면 된다 import { setGlobalOptions } from "firebase-functions/v2/options"; import logger from "firebase-functions/logger"; import { initializeApp } from "firebase-admin/app"; import { getFirestore } from "firebase-admin/firestore"; import { onDocumentUpdated, onDocumentCreated} from "firebase-functions/v2/firestore"; import { getMessaging } from "firebase-admin/messaging"; // functions 초기화 initializeApp(); // 데이터를 조회할 FireStore const db = getFirestore(); const messaging = getMessaging(); // 관련 상수들 const NOT_FOUND_USER = "토큰에 해당하는 유저를 찾을 수 없습니다"; const NOT_FOUND_USER_SETTING = "유저의 토큰, 셋팅값을 찾을 수 없습니다"; const LIKES_ERROR = Array( "좋아요를 누른 사람의 닉네임을 찾지 못했습니다", "유저가 좋아요 알림을 사용 기능을 꺼놓았습니다", "좋아요 알림 메세지를 보내는데 실패했습니다" ); const COMMENTS_ERROR = Array( "포스팅을 찾을 수 없습니다", "댓글 알림기능을 꺼놓았습니다", "댓글 알림 메세지를 보내는데 실패했습니다" ); const type = Array("Likes", "Comments"); const COLLECTION_ROUTE_TOKEN = "token"; const COLLECTION_ROUTE_USERS = "users"; const COLLECTION_ROUTE_POSTS = "notes"; const DOC_ROUTE_POST = "notes/{postId}"; const DOC_ROUTE_COMMENT = "notes/{postId}/comments/{commentsId}"; const notificationMessages = { likes: { title: "새로운 좋아요 도착!", body: (likeUserName, postTitle) => `${likeUserName}님이 게시글 "${postTitle}"에 좋아요❤️를 눌렀습니다` }, comments: { title: "새로운 댓글 도착!", body: (commentAuthorName, postTitle, previewComment) => `${commentAuthorName}님이 게시글 "${postTitle}"에 💬댓글 : "${previewComment}"을 달았습니다` } }; // 유저의 토큰값과 알림 설정 셋팅(on,off 상태값)을 가져옴 async function getUserTokenSettings(userId) { try { // token collection에서 userId에 해당하는 토큰, 알림 셋팅값 가져오기 const tokenDoc = await db .collection(COLLECTION_ROUTE_TOKEN) .doc(userId) .get(); if (!tokenDoc.exists) { logger.error(NOT_FOUND_USER, tokenDoc); return null; } const data = tokenDoc.data(); return { token: data.token, notification_likes: data.notification_likes !== false, notification_comments: data.notification_comments !== false, }; } catch (e) { logger.error(NOT_FOUND_USER_SETTING, e); return null; } } // post(여기에는 notes라고 되어있는데 post라고 보면 됨) collection의 doc이 업데이트 될 때마다 트리거됨 export const sendPostLikesNotification = onDocumentUpdated( DOC_ROUTE_POST, async (event) => { try { const postId = event.params.postId; // 변경 전 데이터 const beforeData = event.data.before.data(); // 변경 후 데이터 const afterData = event.data.after.data(); // 변경 전 좋아요 값 const beforeLikes = beforeData.likes; // 변경 후 좋아요 값 const afterLikes = afterData.likes; // 변경 후 좋아요 값이 변경 전 값과 같거나 작으면 종료 if (afterLikes.length <= beforeLikes.length) return; // 새로운 '좋아요'를 누른 유저의 아이디 필터링 const newLikes = afterLikes.filter( (userId) => !beforeLikes.includes(userId) ); // 새로운 좋아요의 값이 0이면(새로운 좋아요가 없으면) 종료 if (newLikes.length === 0) return; const postAuthorId = afterData.userId; const postTitle = afterData.title; const libraryName = afterData.libraryName; for (const likeUserId of newLikes) { // 작성자가 본인의 글에 좋아요를 누른 경우 - 다음 사용자 처리 if (likeUserId === postAuthorId) continue; let likeUserName = ""; try { // 좋아요를 누른 유저의 정보 가져와서 좋아요를 누른 유저의 아이디 추출 const likeUserData = await db .collection(COLLECTION_ROUTE_USERS) .doc(likeUserId) .get(); if (likeUserData.exists) { likeUserName = likeUserData.data().nickname; } } catch (e) { logger.error(LIKES_ERROR[0], e); } // 사용자의 token, 알림 설정 셋팅 값 가져오기 const userSettings = await getUserTokenSettings(postAuthorId); // token이나 유저의 셋팅값을 찾지 못했을 때 if (!userSettings) { logger.error(NOT_FOUND_USER_SETTING, e); continue; } // 새로운 '좋아요'가 추가될 때 알림을 받는 설정이 off(false)일 때 if (!userSettings.notification_likes) { logger.info(LIKES_ERROR[1]); continue; } // FCM server로 보낼 메세지 const message = { data: { title: notificationMessages.likes.title, body: notificationMessages.likes.body(likeUserName, postTitle), type: type[0], postId: postId, libraryName: libraryName, likeUserId: likeUserId, }, token: userSettings.token, }; await messaging.send(message); } } catch (e) { logger.error(LIKES_ERROR[2], e); } } ); // comment doc이 새로 생성될 때 마다(onDocumentCreated) 트리거됨 export const sendPostCommentsNotification = onDocumentCreated( DOC_ROUTE_COMMENT, async (event) => { try { // 포스트 아이디 const postId = event.params.postId; // 새로운 댓글 아이디 const commentId = event.params.commentsId; // 새로운 댓글 데이터(content, date등...) const commentData = event.data.data(); // 새로운 댓글 작성자 아이디 const commentAuthorId = commentData.userId; // 새로운 댓글 내용 값 const commentContent = commentData.content; let commentAuthorName = ""; // 새로운 댓글을 단 유저의 정보 가져와서 닉네임 추출 try { const commentUserData = await db .collection(COLLECTION_ROUTE_USERS) .doc(commentAuthorId) .get(); if (commentUserData.exists) { commentAuthorName = commentUserData.data().nickname; } } catch (e) { commentAuthorName = "탈퇴한 사용자"; } // 새로운 댓글이 달린 포스트 데이터 가져오기 const postDoc = await db .collection(COLLECTION_ROUTE_POSTS) .doc(postId) .get(); if (!postDoc.exists) { logger.error(COMMENTS_ERROR[0], postId); return; } const postData = postDoc.data(); const postAuthorId = postData.userId; const postTitle = postData.title; // 포스트에 새로 달린 댓글을 포스트 작성자가 쓴 경우 if (commentAuthorId === postAuthorId) return; // 사용자의 token, 알림 설정 셋팅 값 가져오기 const userSettings = await getUserTokenSettings(postAuthorId); if (!userSettings) { logger.error(NOT_FOUND_USER_SETTING, e); return; } // '댓글이 새로 달릴 때 알림받기'가 off(false)일 때 if (!userSettings.notification_comments) { logger.info(COMMENTS_ERROR[1]); return; } // 댓글의 내용의 길이가 10보다 크면 10까지 잘라서 뒤에 "..."를 붙이고 // 아니라면 댓글 내용 그대로를 보여줌 const previewComment = commentContent.length > 10 ? commentContent.substring(0, 10) + "..." : commentContent; // FCM server로 보낼 메세지 객체 const message = { data: { title: notificationMessages.comments.title, body: notificationMessages.comments.body(commentAuthorName, postTitle, previewComment), type: type[1], postId: postId, commentId: commentId, commentUserId: commentAuthorId, }, token: userSettings.token, }; await messaging.send(message); } catch (e) { logger.error(COMMENTS_ERROR[2], e); } } ); // 최대 동시 요청 값 setGlobalOptions({ maxInstances: 10 });다 작성 후에는 firebase deploy --only functions로 함수를 배포해준다 사실 자바스크립트를 너무 오랜만에 해서 좀 이상하기도 했고 functions를 처음 사용해봤기 때문에 관련 API들을 찾고 적용하는게 오래걸렸다 그리고 다른 블로그들도 많이 참고했다! 참고 FCM 아키텍처 개요 | Firebase Cloud Messaging 의견 보내기 FCM 아키텍처 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. FCM은 메시지를 작성, 전송, 수신하는 다음 구성요소 집합을 사용합니다. 메시 firebase.google.com - https://firebase.google.com/docs/cloud-messaging/send-message?hl=ko 앱 서버 전송 요청 작성 | Firebase Cloud Messaging 의견 보내기 앱 서버 전송 요청 작성 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Firebase Admin SDK 또는 FCM 앱 서버 프로토콜을 사용하여 메시지 요청을 작 firebase.google.com Cloud Firestore 트리거 | Cloud Functions for Firebase 의견 보내기 Cloud Firestore 트리거 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Cloud Functions를 사용하면 클라이언트 코드를 업데이트하지 않고도 Cloud Firestor firebase.google.com 함수 관리 | Cloud Functions for Firebase 의견 보내기 함수 관리 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Firebase CLI 명령어를 사용하거나 함수 소스 코드에 런타임 옵션을 설정하면 함수를 배 firebase.google.com 반응형'Android' 카테고리의 다른 글[FCM] FCM으로 notification 구현 - 5 (클라이언트쪽 구현2) (0) 2025.10.27 [FCM] FCM으로 notification 구현 - 4 (클라이언트쪽 구현1) (0) 2025.09.27 [FCM] FCM으로 Notification 구현하기 - 1 (0) 2025.09.23 [Jetpack Compose] 텍스트의 일부분 스타일 변경하기 (2) 2025.08.06 [CameraX] resolution(해상도) 조절 및 fallback 설정 (5) 2025.08.05