본문 바로가기
Frontend2026년 3월 10일8분 읽기

웹 푸시 알림 구현 — Firebase FCM과 Service Worker

YS
김영삼
조회 457

웹 푸시 알림 아키텍처

웹 푸시 알림은 브라우저가 닫혀 있어도 사용자에게 알림을 보낼 수 있는 기능입니다. 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 APINotification비고
Chrome지원지원완전 지원
Firefox지원지원완전 지원
Edge지원지원완전 지원
Safari16.4+16.4+macOS Sonoma+, iOS 16.4+
  • FCM 토큰은 주기적으로 갱신되므로 onTokenRefresh를 구현해야 함
  • HTTPS 환경에서만 Service Worker와 Push API 동작
  • 알림 권한 요청은 사용자 상호작용(클릭 등) 후에 호출하는 것이 UX상 좋음
  • iOS Safari는 PWA로 설치된 경우에만 웹 푸시 지원
  • 토큰 관리: 만료/비활성 토큰을 정기적으로 정리하는 배치 작업 필요

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록