웹 푸시 알림 아키텍처
웹 푸시 알림은 브라우저가 닫혀 있어도 사용자에게 알림을 보낼 수 있는 기능입니다. Service Worker와 Push API, Firebase FCM을 결합하여 구현합니다.
동작 흐름
1. 사용자가 알림 권한 허용
2. Service Worker 등록 + FCM 토큰 발급
3. 서버에 FCM 토큰 저장
4. 서버 → FCM → 브라우저 Push 전송
5. Service Worker가 Push 이벤트 수신
6. Notification API로 알림 표시
[서버] → [FCM 서버] → [브라우저 Push Service] → [Service Worker] → [알림]
Firebase 설정
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';
const firebaseConfig = {
apiKey: "AIza...",
authDomain: "myapp.firebaseapp.com",
projectId: "myapp",
messagingSenderId: "123456789",
appId: "1:123456789:web:abc123"
};
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
export async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
const token = await getToken(messaging, {
vapidKey: 'BEl62iUYgU...'
});
await fetch('/api/notifications/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
return token;
}
throw new Error('Notification permission denied');
}
onMessage(messaging, (payload) => {
showToast(payload.notification.title, payload.notification.body);
});
Service Worker 설정
// public/firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.8.0/firebase-messaging-compat.js');
firebase.initializeApp({
apiKey: "AIza...",
projectId: "myapp",
messagingSenderId: "123456789",
appId: "1:123456789:web:abc123"
});
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
const { title, body, icon, click_action } = payload.notification;
self.registration.showNotification(title, {
body: body,
icon: icon || '/icon-192.png',
badge: '/badge-72.png',
tag: 'notification-' + Date.now(),
data: { url: click_action || '/' },
actions: [
{ action: 'open', title: '열기' },
{ action: 'dismiss', title: '닫기' }
]
});
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'dismiss') return;
const url = event.notification.data.url;
event.waitUntil(
clients.matchAll({ type: 'window' }).then((clientList) => {
for (const client of clientList) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
return clients.openWindow(url);
})
);
});
서버 측 전송 (Node.js)
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
async function sendToUser(token, title, body, url) {
const message = {
token: token,
notification: { title, body },
webpush: {
notification: {
icon: '/icon-192.png',
click_action: url
},
fcm_options: { link: url }
}
};
try {
const response = await admin.messaging().send(message);
console.log('Successfully sent:', response);
} catch (error) {
if (error.code === 'messaging/registration-token-not-registered') {
await deleteToken(token);
}
}
}
async function sendToTopic(topic, title, body) {
await admin.messaging().send({
topic: topic,
notification: { title, body }
});
}
브라우저 지원 현황
| 브라우저 | Push API | Notification | 비고 |
|---|---|---|---|
| Chrome | 지원 | 지원 | 완전 지원 |
| Firefox | 지원 | 지원 | 완전 지원 |
| Edge | 지원 | 지원 | 완전 지원 |
| Safari | 16.4+ | 16.4+ | macOS Sonoma+, iOS 16.4+ |
- FCM 토큰은 주기적으로 갱신되므로
onTokenRefresh를 구현해야 함 - HTTPS 환경에서만 Service Worker와 Push API 동작
- 알림 권한 요청은 사용자 상호작용(클릭 등) 후에 호출하는 것이 UX상 좋음
- iOS Safari는 PWA로 설치된 경우에만 웹 푸시 지원
- 토큰 관리: 만료/비활성 토큰을 정기적으로 정리하는 배치 작업 필요
댓글 0