지난 3월 구름톤에서 웹 푸시알림으로 특정 이벤트 발생을 유저에게 알려주는 기능을 구현하게 되었다.
백엔드 팀원 역시 FCM을 처음 시도하는 것이라 일주일이라는 기간동안 가능할까 걱정하였는데 다행히 성공적으로 구현해 당일 시연까지 할 수 있었다.
Fire-Cloud-Messaging의 약자로 메시지를 안정적으로 무료 전송할 수 있는 크로스 플랫폼 메시징 솔루션 이라고 소개된다.
주요기능은 다음과 같다
Android, iOS, 웹 등 플랫폼에 따라 푸시알림을 보내려면 각 플랫폼 환경별로 개발해야하는 불편함이 존재한다. (Android - GCM (Google Cloud Messaging), iOS - APNS (Apple Push Notification service))
하지만 FCM은 교차 플랫폼 메시지 솔루션이기 때문에 플랫폼에 종속되지 않고 푸시알림을 구현할 수 있으며 효율적인 관리가 가능하다.
위 그림은 FCM 아키텍쳐로 FCM 서버차원에서 각 플랫폼에 맞는 방식으로 메시지 전송을 수행하고 있음을 볼 수 있다.
우리 서비스의 경우에는 그룹의 리더가 공지를 올리면 모든 멤버에게 알림을 보내는 기능을 구현해야한다.
이 기능을 만약 서버를 경유해 알림을 보내야한다면
위의 흐름으로 진행되어야 하는데, 알림을 받기위해서는 B가 항상 서버에 접속해야하고 이는 많은 배터리와 네트워크 사용이라는 문제를 발생시킨다.
하지만 FCM과 같은 클라우드 메시징 서버를 사용한다면
이처럼 사용자가 어플리케이션 서버에 접속해있지 않더라도 클라우딩 메시징 서버로부터 실시간으로 메시지를 전송받을 수 있다는 점에서 리소스 낭비를 줄일 수 있다.
기초 세팅은 이 글을 참고하였다
참고로 Next.js 14 환경이다... 하지만 구조는 13과 크게 다른 점을 느끼지 못했다
SDK를 추가 및 초기화해주는 단계이다
importScripts('https://www.gstatic.com/firebasejs/9.14.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.14.0/firebase-messaging-compat.js');
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
랜딩화면에 넣어줄 컴포넌트에 알림 허용 + 사용자의 디바이스 토큰을 받아오기 + 페이로드에서 메시지 받기를 진행한다
const Index = () => {
const router = useRouter();
const onMessageFCM = async () => {
// 브라우저에 알림 권한 요청
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
const firebaseApp = initializeApp({
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
});
const messaging = getMessaging(firebaseApp);
// 인증서 키 값
getToken(messaging, { vapidKey: process.env.NEXT_PUBLIC_FIREBASE_KEY_PAIR })
.then((currentToken) => {
if (currentToken) {
localStorage.setItem('device', currentToken);
} else {
console.log('No registration token available. Request permission to generate one.');
}
})
.catch((err) => {
console.log('An error occurred while retrieving token. ', err);
router.refresh();
});
onMessage(messaging, (payload) => {
console.log('Message received. ', payload);
});
};
useEffect(() => {
onMessageFCM();
}, []);
return <></>;
};
문제는 로컬환경에서는 키 값을 전부 환경변수로 대체해도 괜찮았는데...
배포를 하니 해당 에러가 나타났다. 왜인지는 모르겠으나 다른 포스팅을 보아도 배포 후에는 초기화부분에서 환경변수가 제대로 적용이 안 되는 것 같다.
따라서 해당 파일에서는 환경변수를 사용하지 않고 필요한 부분만 값을 넣어주어야 한다.
const firebaseConfig = {
apiKey: 'api-key',
projectId: 'project-id',
messagingSenderId: 'sender-id',
appId: 'app-id',
};
에러 2
GET http://localhost:5000/resources/tutorial/css/example.css net::ERR_ABORTED 404 (Not Found)
이 글을 참고하여 import문 수정을 진행하였다
에러3
An error occurred while retrieving token. DOMException: Failed to execute 'subscribe' on 'PushManager': Subscription failed - no active Service Worker
이 문제의 경우 첫 시도에서만 발견되고 이후에는 문제가 없음을 알 수 있었다
해당 글을 참고해서 firebase 초기화 파일에는 환경변수가 아닌 그냥 값을 넣게 되었다.
성공적으로 클라이언트는 루트 페이지에서 유저의 SDK를 초기화하고, 회원가입 시 서버에 firebase에서 받아온 유저의 FCM토큰을 함께 전달해주었다.
이후 공지사항이나 유저 가입이 발생하면 백엔드측에서 관련된 유저의 FCM토큰과 Firebase Functions을 사용해 알림의 내용을 지정하고 푸시알림 요청을 전송한다.
사실 프로젝트를 하면서 Firebase에 직접적으로 요청을 보내는 것은 백엔드이기 때문에 프론트엔드에서의 복잡함은 크지 않다.. 나중에 백엔드까지 구현해보는 기회가 있다면 추가 포스팅을 하면 좋을 듯 하다.