0. PWA와 FCM을 쓰려는 이유
친구들과 공모전을 나가기로 했었다. 공모전에 프로젝트를 제출하려면 몇 가지 조건을 충족해야했는데, iOS와 Android에서 모두 작동해야하며, 푸시 알림이 구현되어야 한다는 조건이었다. React와 문법이 비슷한 React Native를 빠르게 학습한 후 이 언어를 활용하여 개발을 진행하는 방법이 먼저 떠올랐다. 그 다음에 떠오른 것이 바로 PWA(Progressive Web App)였다.
PWA 기술을 활용하면 웹사이트지만 네이티브 앱처럼 설치하여 사용할 수 있고, 푸시 알림, 오프라인 작동 등 앱과 유사한 기능들도 구현할 수 있다. 푸시 알림은 FCM(Firebase Cloud Messaging)을 통하여 구현하기로 했다. Firebase을 독학하여 CRUD를 구현해본 적은 있지만 푸시 알림은 구현해본 적이 없었기 때문에 이번 기회에 FCM을 사용해보고 싶었다.
1. ‘vite-pwa’를 통하여 PWA를 구현하자
PWA를 어떻게 구현할 수 있을지 찾아보니, 어떤 스택을 활용하여 프로젝트를 빌드하느냐에 따라 PWA 구현 방법도 달라졌다. Next.js를 활용하면 ‘next-pwa’ 라이브러리를, Vite를 활용하면 ‘vite-pwa’ 라이브러리를 활용하면 되었다.
무엇을 사용할까 고민하다가, ‘지금 내가 구현하려는 프로젝트가 SSR(Server Side Rendering)이 꼭 필요한가?’ 하는 의문이 들었다. 사람들이 Next.js를 사용하는 이유가 SSR을 구현하기 위함이 전부가 아님을 알고 있다. 하지만 내가 구현하려는 프로젝트는 SEO가 중요하지 않고, 또 백엔드 팀원들이 존재한다. 그래서 Next.js를 사용하지 않고 가벼운 vite를 활용하기로 했다.
FCM을 구현하기 전 먼저 PWA부터 구현했었는데, vite-pwa는 service worker를 자동으로 생성해주기 때문에 별도의 service worker 파일을 만들어줄 필요가 없다고해서 따로 생성하지 않았다. (물론 뒤에 FCM을 구현할 때에는 service worker 파일을 생성했다.) 그 대신 vite.config.ts에서 VitePWA 설정을 추가해주었다.
vite.config.ts
plugins: [
react({
jsxImportSource: '@emotion/react',
}),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true,
type: 'module'
},
manifest: {
name: '앱 이름',
short_name: '앱 이름',
description: '앱 설정',
theme_color: '#ffffff',
icons: [
{
src: '/favicon/favicon-16x16.png',
sizes: '16x16',
type: 'image/png'
},
{
src: '/favicon/favicon-32x32.png',
sizes: '32x32',
type: 'image/png'
},
(...)
]
}
})
],
})
확인 결과 sw.js를 따로 사용하지 않아도 PWA가 구현이 되었다! 어떤 환경에서는 앱 설치가 되지 않는 문제가 발생했지만, 계속 새로고침을 반복하다보니 다른 환경에서도 앱 설치가 가능했다. 이렇게 일단 PWA를 구현하는데는 성공했고, 이제 FCM을 구현해보기로 하였다.
2. FCM을 구현해보자
구글링을 해보면 어떻게 FCM을 구현할 수 있는지 상세히 작성되어 있는 글들을 확인할 수 있다. 또한 공식 문서에도 설명이 자세히 되어 있으니 이 글을 참고해서 FCM 설정을 시작했다.
자바스크립트 Firebase 클라우드 메시징 클라이언트 앱 설정 | Firebase Cloud Messaging
2024년 데모 데이에서, Firebase를 사용하여 AI 기반 앱을 빌드하고 실행하는 방법에 관한 데모를 시청하세요. 지금 시청하기 의견 보내기 자바스크립트 Firebase 클라우드 메시징 클라이언트 앱 설정
firebase.google.com
2-1. Service Worker 설정
firebase-messaging-sw.js를 생성해서 아래 코드와 같이 service worker를 설정했다.
firebase-messaging-sw.js
// Service Worker에서 사용할 Workbox 매니페스트
self.__WB_MANIFEST
importScripts('<https://www.gstatic.com/firebasejs/9.0.0/firebase-app-compat.js>');
importScripts('<https://www.gstatic.com/firebasejs/9.0.0/firebase-messaging-compat.js>');
// Firebase 초기화
firebase.initializeApp({
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: ""
});
const messaging = firebase.messaging();
// Background 메시지 처리
messaging.onBackgroundMessage((payload) => {
console.log('Background 메시지 수신:', payload);
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/logo.png'
};
self.registration.showNotification(notificationTitle, notificationOptions);
});
service worker 파일을 public 디렉토리에 추가한 이유
AI가 service worker 파일은 public 디렉토리에 추가하라고 해서 public 디렉토리에 추가했다.
이유를 물어보니,
- 루트 경로 접근성: 서비스 워커는 보안상의 이유로 반드시 웹사이트의 루트 경로에서 접근 가능해야 합니다. public 폴더의 내용은 빌드 시 프로젝트의 루트 디렉토리로 복사되어 서비스됩니다.
- 스코프 제한: 서비스 워커는 자신이 위치한 디렉토리와 그 하위 디렉토리에만 영향을 미칠 수 있습니다. 루트에 위치함으로써 전체 애플리케이션에 대한 푸시 알림을 처리할 수 있습니다.
- 직접 서빙: public 폴더의 파일들은 번들링이나 트랜스파일링 없이 있는 그대로 서빙됩니다. 서비스 워커는 클라이언트 사이드 JavaScript와는 별도로 동작해야 하므로, 번들러의 처리 없이 그대로 서빙되어야 합니다.
라고 한다.
public 폴더에 있으면 문제점이 하나 생기는데, 바로 .env 파일의 환경변수를 사용하지 못한다는 점이다.
빌드 스크립트를 작성해서 환경변수를 주입하는 방법도 사용할 수 있겠으나, 검색해보니 firebase의 키 값들은 노출되도 별 문제가 없다는 답변이 많아 위 코드와 같이 초기화를 진행했다.
Workbox 매니페스트
먼저 Workbox 매니페스트를 선언했다. Workbox 매니페스트는 생소한 개념이라 어떤 용도로 사용되는지 잘 몰랐다. 찾아본 결과, 웹사이트를 빌드(build)할 때, self.__WB_MANIFEST는 실제 파일 목록으로 바뀐다고 한다.이 매니페스트가 캐시할 파일 목록들을 저장해놓기 때문에, 인터넷이 끊겨도 웹사이트가 작동할 수 있게 해주고, 파일들을 미리 저장해두어 웹사이트 로딩 속도를 빠르게 해준다고 한다.
그리고 밑에는 백그라운드 메시지 처리 코드들이 있다. FCM 알림은 백그라운드(Background) 알림과 포그라운드(Foreground) 알림으로 나뉘는데, 포그라운드 알림은 앱이 켜져 있을 때 오는 알림이고, 백그라운드 알림은 앱이 꺼져 있을 때 오는 알림이다. service worker는 앱이 꺼져 있을 때에도 실행될 수 있는 유일한 컨텍스트이므로, 백그라운드 알림 관련 코드는 service worker 파일에 추가하는 것이 맞아보인다.
2-2. firebase.ts 생성
firebase.ts를 생성해줘서 firebase 초기화 및 설정을 진행했다.
잠깐만, 위의 service worker 파일에서 firebase 초기화를 이미 진행해준거 아닌가, 왜 또 진행하지 하는 의문을 가질 수 있는데 찾아본 결과,
- Service Worker는 완전히 독립된 JavaScript 실행 환경
- 메인 앱의 JavaScript 컨텍스트와 변수나 상태를 공유할 수 없음
때문에 firebase 초기화를 각각 해줘야 한다고 한다.
firebase.ts
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
};
// Firebase 초기화
const app = initializeApp(firebaseConfig);
console.log('Firebase initialized:', app);
const messaging = getMessaging(app);
console.log('Messaging initialized:', messaging);
// FCM 토큰 가져오기
export const requestForToken = async () => {
try {
const currentToken = await getToken(messaging, {
vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
});
if (currentToken) {
console.log('FCM 토큰 가져오기 성공!')
// 테스트 시 사용
// console.log('FCM 토큰 값: ', currentToken);
// 나중에 토큰을 백엔드 서버로 전송하는 로직 추가
return currentToken;
} else {
console.log('토큰을 가져올 수 없습니다.');
}
} catch (err) {
console.log('토큰 발급 중 에러 발생:', err);
}
};
// foreground 메시지 수신
export const onMessageListener = () =>
new Promise((resolve) => {
onMessage(messaging, (payload) => {
console.log('Foreground message received:', payload);
resolve(payload);
});
});
FCM 토큰을 가져오는 requestForToken 메서드와 포그라운드 메시지를 수신하는 메서드도 firebase.ts에서 생성해주었다.
주목할 점은 FCM 토큰을 가져올 때는 vapidKey를 담아서 getToken으로 보내줘야 한다는 것이다.
또한 firebase.ts는 src 파일에 있으므로 환경변수들을 활용해서 키들을 공개하지 않으려 했다. (물론 service worker에 다 공개되 있으므로 크게 의미는 없지만..)
2-3. App.tsx에서 코드 추가
최상단 파일인 App.tsx에서 useEffect를 활용하여 앱이 실행될 때마다 알림 권한을 요청하고, FCM 토큰을 요청해서 FCM 토큰을 받아오게 했다. 코드에서 확인할 수 있듯이 permission이 ‘granted’여야 FCM 토큰을 받을 수 있다.
그리고 firebase.ts의 onMessageListener 메서드를 활용하여 포그라운드 메시지가 작동하게 코드를 작성하였다.
App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { useEffect } from 'react';
import Onboarding from './pages/Onboarding';
import MyPage from './pages/MyPage';
import Register from './pages/Register';
import { requestForToken, onMessageListener } from './firebase/firebase';
function App() {
useEffect(() => {
// 알림 권한 요청
if ('Notification' in window) {
console.log('Current notification permission:', Notification.permission);
Notification.requestPermission().then((permission) => {
console.log('알림 허용이 되어있나요 :', permission);
if (permission === 'granted') {
// FCM 토큰 요청
requestForToken().then((token) => {
if (token) {
console.log('requestForToken 성공!');
}
});
}
if (permission === 'denied') {
console.log('알림이 거부되었어요');
}
});
} else {
console.log('알림이 되지 않아요!');
}
// Foreground 메시지 수신 처리
onMessageListener()
.then((payload : any) => {
// Foreground에서 알림 표시
if (Notification.permission === 'granted') {
new Notification(payload.notification.title, {
body: payload.notification.body,
icon: '/logo.png'
});
}
if (Notification.permission === 'denied') {
console.log('알림이 거부되었어요!');
}
})
.catch((err) => console.log('메시지 수신 에러:', err));
}, []);
return (
<BrowserRouter>
<Routes>
<Route path='/onboarding' element={<Onboarding />} />
<Route path='/myPage' element={<MyPage />} />
<Route path='/register' element={<Register />} />
<Route path='/' element={<Navigate to='/onboarding' replace />} />
</Routes>
</BrowserRouter>
);
}
export default App;
3. FCM이 잘 작동하는지 테스트해보자
firebase 홈페이지에서 알림을 보내서 FCM이 잘 작동하는지 테스트할 수 있다.
테스트 알림 메세지 문서 링크:
https://firebase.google.com/docs/cloud-messaging/js/first-message?hl=ko
위 링크로 접속해서 테스트 알림 메시지를 보내는 방법이 나오는데, 아래와 같이 따라하면 된다.
FCM 등록 토큰을 넣어야되는데, firebase.ts에서 토큰을 가져오는 메서드를 만들 때 임시로 토큰을 알려줄 수 있도록 코드를 수정하면 된다.
// FCM 토큰 가져오기
export const requestForToken = async () => {
try {
const currentToken = await getToken(messaging, {
vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
});
if (currentToken) {
console.log('FCM 토큰 가져오기 성공!')
// 테스트 시 사용
// console.log('FCM 토큰 값: ', currentToken);
// 나중에 토큰을 백엔드 서버로 전송하는 로직 추가
return currentToken;
} else {
console.log('토큰을 가져올 수 없습니다.');
}
} catch (err) {
console.log('토큰 발급 중 에러 발생:', err);
}
};
주석 친 ‘테스트 시 사용’ 밑의 코드를 활성화 시키고. 나온 토큰 값을 복사해서 웹 페이지 작성란에 넣자.
그리고 알려준대로 차근차근 진행했다.
그리고 콘솔을 확인해보면 알림이 잘 왔음을 확인할 수 있다!
이번에는 포그라운드 메시지가 잘 도착하는지 테스트 해봤는데, 다음에는 백그라운드 메시지가 잘 도착했는지 테스트해보려 한다.
아직 FCM과 관련해서 백엔드와 연동을 진행하지는 않았기에 그 때에도 FCM이 잘 작동할지는 확신할 수 없지만, 계속 디벨롭해가면서 푸시 알림이 잘 작동되는 PWA를 완성해보려 한다.
'Frontend > React' 카테고리의 다른 글
'react-datepicker'에서 날짜 상태를 관리할 때 겪었던 시행착오 (0) | 2024.08.03 |
---|---|
프로젝트 팀원 코드를 보고 컴포넌트 만드는 법 배우기(typescript + clsx) (0) | 2024.07.21 |
[React, TS] 프로젝트를 진행하며 상태 관리에 대해 알게 된 것들 (1) | 2024.01.13 |
[React] useEffect()에 대해서 (0) | 2023.08.17 |
[React] Props에 대해서 (0) | 2023.08.17 |