회사에서 push 알림 기능이 들어갈수도 있다해서 간단하게 구현해본 내용을 정리하였습니다.(결국 들어가진 않았지만.. 흑흑) 은근 자료찾기가 까다로워서 저처럼 헤맸던 개발자가 없기를 바라며.. 작성해보았습니다.
FCM이란
Firebase Cloud Messaging(FCM)이란 메시지를 안정적으로 무료 전송할 수 있는 크로스 플랫폼 메시징 솔루션입니다. 기존에는 푸시 메세지를 보내기 위해 플랫폼 별로 개발해야하는 불편함이 있었지만 FCM의 등장으로 쉽게 개발할 수 있게 되었습니다.
위 이미지는 maejing님의 티스토리에 소개된 FCM의 동작 방식을 쉽게 알려주는 그림이라 가져와보았습니다. 웹의 경우도 동일합니다.
먼저 Firebase 서버에 토큰을 요청하고 획득한 뒤 우리 서버에 토큰을 저장합니다. 해당 토큰을 이용해 메시지 전송을 요청하면 Firebase 서버에서 메시지를 전송해줍니다. 그리고 클라이언트 앱에서 리스너를 통해 메시지를 수신하는 것입니다.
구현해보기
Firebase 프로젝트 추가
- Firebase Console에서 프로젝트 만들기 클릭
- 앱 이름을 작성하고 약관에 동의한 뒤 계속 버튼 클릭
- 구글 애널리틱스 사용 여부 동의한 뒤 계속 버튼 클릭
- 구글 애널리틱스 계정 선택한 뒤 프로젝트 만들기 클릭
앱 등록
- 프로젝트 페이지 중앙에 있는 웹 아이콘(</>)을 클릭합니다.
- 앱 이름을 작성하고 등록을 눌러줍니다.
- 여기서 Firebase SDK 추가 단계에서 npm 탭에 나와있는 코드를 사용하면 되는데요. firebaseConfig 부분을 복사해주세요.
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// 각자 발급받은 값이 들어있습니다 :)
const firebaseConfig = {
apiKey: '',
authDomain: '',
projectId: '',
storageBucket: '',
messagingSenderId: '',
appId: '',
measurementId: '',
};
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
Firebase 초기화 코드 추가
홈페이지에서의 세팅이 끝났으니 프로젝트로 돌아와 Firebase 초기화 코드를 추가해주겠습니다.
- 먼저 각자의 패키지 매니저로 firebase 모듈을 설치해주겠습니다. (저는 yarn을 사용했습니다.)
yarn add firebase
- config 파일을 만들어 설정 코드를 추가해줍니다. (저는
config/firebase.ts
경로를 만들어 추가하였습니다.)
import { initializeApp, getApps, FirebaseApp } from 'firebase/app';
import { getMessaging, Messaging } from 'firebase/messaging';
const firebaseConfig = {
apiKey: '',
authDomain: '',
projectId: '',
storageBucket: '',
messagingSenderId: '',
appId: '',
measurementId: '',
};
const initializeFirebase = () => {
let firebaseApp: FirebaseApp | null = null;
let messaging: Messaging | null = null;
return () => {
if (typeof window !== 'undefined' && !getApps().length) {
firebaseApp = initializeApp(firebaseConfig);
messaging = getMessaging(firebaseApp);
}
return { firebaseApp, messaging };
};
};
const getFirebaseInstance = initializeFirebase();
export { getFirebaseInstance };
- 추후 firebase와 관련된 값들이 추가될 수 있으므로 작업을 용이하게 하기 위해 hooks/useFirebase.ts에 훅을 추가하여 관리해주었습니다. (저희는 React 기반이라 Hook을 사용했는데 이부분은 선택입니다.)
import { FirebaseApp } from 'firebase/app';
import { Messaging } from 'firebase/messaging';
import { useEffect, useState } from 'react';
import { getFirebaseInstance } from '@/config/firebase';
export const useFirebase = () => {
const [firebaseApp, setFirebaseApp] = useState<FirebaseApp | null>(null);
const [messaging, setMessaging] = useState<Messaging | null>(null);
useEffect(() => {
const { firebaseApp, messaging } = getFirebaseInstance();
if (firebaseApp && messaging) {
setFirebaseApp(firebaseApp);
setMessaging(messaging);
}
}, []);
return { firebaseApp, messaging };
};
- firebase app을 초기화할때 firebase-messaging-sw.js 라는 서비스 워커 파일을 브라우저에 등록하는 과정이 자동으로 이루어집니다. 따라서 이 파일이 없으면 에러가 나므로 빈 파일을 만들어 public 경로 하위에 넣어줍니다. (해당 파일의 내용은 나중에 추가될 예정입니다)
//public/firebase-messaging-sw.js
{}
브라우저가 서비스 워커 파일에 직접 접근할 수 있어야 하므로 꼭 정적인 public 폴더 하위에 작성해줍니다.
메세지 수신 로직 작성
이제 초기화가 끝났으니 메세지를 받아봐야겠죠!
메세지를 받을 때에는 두가지 경우가 있습니다. 첫번째는 foreground(사용자의 focus가 들어간 경우)이며 두번째는 background(사용자가 다른 탭을 클릭하고 있거나 브라우저가 닫힌 경우)입니다.
forground에서 메시지 수신하기
foreground에서 메시지를 수신할 때는 onMessage 라는 함수를 사용합니다. 메세지를 수신할 페이지에서 아래 코드를 추가해줍니다.
useEffect(() => {
if (!messaging) return;
const unsubscribe = onMessage(messaging, (payload) => {
console.log('ForegroundMessage', payload);
});
return () => {
unsubscribe();
};
}, [messaging]);
이렇게 하여 messaging 객체가 변할때마다 onMessage를 설정할 수 있습니다. 다만 useEffect 내에서 메세지 이벤트를 구독하고 있기 때문에 컴포넌트가 언마운트 될 때 정리할 수 있도록 return해줍니다.
background에서 메시지 수신하기
background에서는 어떻게 메시지를 수신할까요? 이때 등장하는 개념이 서비스워커입니다. 서비스 워커는 메인 스레드와 다른 스레드에서 동작하므로 백그라운드에서 처리해야 할 일들을 해줄 수 있습니다.
위에서 firebase app을 초기화할 때 만들었던 firebase-messaging-sw.js 를 아래와 같이 수정합니다.
importScripts('https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js');
importScripts(
'https://www.gstatic.com/firebasejs/8.10.1/firebase-messaging.js',
);
const firebaseConfig = {
apiKey: '',
authDomain: '',
projectId: '',
storageBucket: '',
messagingSenderId: '',
appId: '',
measurementId: '',
};
const app = firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
messaging.onBackgroundMessage(function (payload) {
console.log('onBackgroundMessage', payload);
const title = 'Default Title';
const options = {
body: 'Default body',
};
console.log('Push event received:', title, options);
self.registration.showNotification(title, options);
});
아까 말씀드렸듯이 서비스 워커는 다른 스레드에서 동작하기 때문에 앱에서 초기화했다 하더라도 이곳에서 따로 초기화해주어야 합니다. (이부분이 헷갈렸었던...)
백그라운드에서 메세지를 수신할 때는 onBackgroundMessage
라는 함수를 사용합니다. showNotification
이라는 함수에 알림 팝업에 보여줄 내용을 전달해줍니다. title이 팝업의 제목, options.body에 들어간 텍스트가 팝업의 내용이 됩니다.
테스트 메세지 전송해보기
이제 메세지를 받을 준비가 되었습니다. 테스트 메세지를 전송해보겠습니다. 이를 위해서 크게 두가지 방법이 있습니다. 첫번째는 Firebase console에 들어가서 전송해보는 것이고 두번째는 직접 서버를 구현해보는 것입니다.
Firebase console에서 전송해보기
- firebase console 메인에 접속합니다.
- 내가 생성한 프로젝트를 클릭하여 들어갑니다.
- 왼쪽 메뉴에서 Messaging이라는 메뉴를 클릭하여 들어갑니다.
- 화면 중앙의 새 캠페인이라는 버튼을 클릭하면 버튼 옆에 작은 새창이 뜨는데 알림을 클릭합니다.
- 제목과 내용을 입력한 뒤 우측의 테스트 메시지 전송 버튼을 클릭합니다.
- FCM 등록 토큰 추가 입력창에 getToken 함수를 통해 부여받은 토큰을 추가해준 뒤 테스트를 누르면 메세지가 전송됩니다.
서버 구현해보기
서버는 백엔드 개발자들이 구현해주시는거지만 테스트를 위해 직접 구현해보았습니다. 서버 구현에는 두가지 방법이 있는데 저는 더 간단한 방법인 firebase admin sdk
를 사용하여 구현하였습니다.
(더 디테일한 설정이 가능한 FCM REST API를 통한 설정을 원하신다면 문서를 참고해주세요. 보통 실제 서버는 이걸 통해 구현해주시지 않을까 싶습니다.)
- 생성한 app으로 들어간 뒤 서비스 계정 탭을 클릭하고 새 비공개 키 생성을 해줍니다.
- 비공개 키를 생성하면 키가 담긴 json 파일을 다운받게 됩니다. serviceAccountKey.json 으로 이름을 바꾸어줍니다. (제가 만든 서버랑 맞추기 위해서라 이름은 원하시는대로 하시면 됩니다!)
- 간단한 노드 서버를 만들기 위해 프로젝트를 세팅해줍니다. (npm init 같은 부분은 생략하겠습니다) 이후 app.js 파일을 만들어 아래와 같이 넣어줍니다.
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
const express = require('express');
const cors = require('cors');
const app = express();
const port = 4000;
app.use(cors());
app.use(express.json());
app.get('/', (req, res) => {
res.send('Hello Firebase Admin!');
});
app.post('/send-push', (req, res) => {
const token = req.body.token;
const message= {
notification: {
title: '제목',
body: '내용'
},
token
};
admin.messaging().send(message)
.then((response) => {
console.log('Successfully sent message:', response);
res.status(200).json({ message: 'Push notification sent successfully.' });
})
.catch((error) => {
console.log('Error sending message:', error);
res.status(500).json({ message: 'Error sending push notification.', error: error.message });
});
});
app.post('/send-message', async (req, res) => {
const token = req.body.token;
const message = {
to: token,
notification: {
title: "제목",
body: "내용"
},
data: {
}
};
try {
} catch (error) {
console.error('Error sending Message:', error);
res.status(401).json({ message: 'Unauthorized' });
}
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
- 프로젝트 최상단에 아까 저장한 serviceAccountKey.json을 넣어줍니다.
- 이제 실행해주면 됩니다! localhost:4000 으로 서버가 실행됩니다.
node app.js
- 이제 클라이언트 앱에서 아래와 같이 작성하여 메세지를 전송하면 됩니다.
const sendMessage = () => {
if (!messaging) return;
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
getToken(messaging, {
vapidKey: VAPID_KEY,
})
.then((currentToken) => {
if (currentToken) {
console.log(currentToken);
fetch('http://localhost:4000/send-push', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: currentToken,
}),
})
.then((response) => response)
.then((data) => {
console.log('Server Response:', data);
})
.catch((error) => {
console.error('Error:', error);
});
} else {
console.log(
'No registration token available. Request permission to generate one.',
);
}
})
.catch((err) => {
console.log(
'An error occurred while retrieving token. ',
err,
);
});
}
});
};
덧붙이기
messaging 객체가 무엇인지?
messaging 객체가 담고 있는 정보는 클라이언트 앱이 FCM과 통신할 때 필요한 인증정보(config에 넣어주었던 API 토큰 등)와 클라이언트 앱의 상태(알림을 허용했는지 여부) 등입니다. 이는 getToken으로 토큰을 가져올 때 필요한 정보라서 첫번째 인자로 넣어주는 것입니다. 이렇게 Messaging 객체를 넘겨주면 getToken이 messaging 객체의 컨텍스트 내에서 동작하게 됩니다.
Notification API로 getToken 함수를 감싸준 이유
firebase 공식문서에서는 getToken 함수만 언급이 되어있습니다. 처음에 그렇게만 구현을 했었는데요. getToken 함수를 실행하면 기본적으로 알림을 허용할거냐는 브라우저 팝업을 최초에 띄워주는데요.
만약 not now를 선택하면 이후부터 계속 catch 블록에 에러처럼 잡히는 문제점이 있었습니다. 이러면 사용자가 허용을 안해서 에러가 난건지 토큰을 get해오다가 에러가 난건지 구분을 못하겠지요?
그래서 웹에서 기본으로 제공해주는 api인 Notification을 통해 알림을 허용할것인지를 별도로 묻도록 코드를 수정했습니다.
주의주의 해야할 어이없는 실수
작업을 하는데 팝업 알림이 어떻게 해도 뜨지 않았습니다. 별의별 방법을 다 써봤는데도 안됐는데 알고보니 브라우저의 알림 설정이 꺼져있었습니다. 여러분은 방해금지 모드가 설정되어있거나 브라우저 알림 설정이 꺼져 있는지 꼭 확인하시길 바랍니다!
'트러블슈팅' 카테고리의 다른 글
github packages로 private하게 패키지 배포하기 (+다운로드 방법) (2) | 2024.10.29 |
---|---|
algolia로 검색 기능 구현하기 (+ Docusaurus) (4) | 2024.10.28 |
맥에서 vs code로 스프링 프로젝트 띄우기-2 (0) | 2020.05.09 |
맥에서 vs code로 스프링 프로젝트 띄우기-1 (0) | 2020.05.09 |