Express.js Web Push
1. Web Push 개요
Web Push는 브라우저가 서버로부터 비동기 알림을 수신하는 메커니즘이다.
특징은 다음과 같다.
- 웹 페이지가 열려 있지 않은 상태에서도 알림 수신 가능
- 데스크톱, 모바일 브라우저 공통 동작
- 브라우저 벤더가 운영하는 Push Service를 경유
- Service Worker, Push API, VAPID 기반 표준 기술
서버는 브라우저에 직접 메시지를 전달하지 않는다.
서버 → Push Service → 브라우저(Service Worker)
Push Service는 메시지 전달 신뢰성과 확장성을 담당한다.
2. 동작 흐름
- 브라우저에서 Service Worker 등록
- 브라우저가 Push Service에 구독 요청
- 구독 정보(endpoint, keys)를 서버로 전송
- 서버가 구독 정보 저장
- 서버가 Push Service로 메시지 전송
- Push Service가 Service Worker 실행
- Service Worker가 알림 표시
이 흐름을 기준으로 서버와 클라이언트 구현을 구성한다.
3. 서버 저장 스키마
Web Push 구독은 사용자 기준이 아니라 브라우저 및 디바이스 기준으로 관리된다. 하나의 사용자는 여러 개의 subscription을 가질 수 있다.
push_subscriptions
push_subscriptions
id bigint PK
user_id bigint
endpoint text UNIQUE
p256dh varchar(255)
auth varchar(255)
user_agent varchar(255)
is_active boolean
created_at timestamp
updated_at timestamp
컬럼 설명
- endpoint: Push Service URL, 구독 식별자
- p256dh / auth: payload 암호화 키
- user_id: 사용자 기준 발송을 위한 참조 값
- is_active: 만료 또는 실패 구독 관리용 플래그
4. 서버 코드 예제 (Express.js)
const express = require('express');
const webPush = require('web-push');
const {
saveSubscription,
getActiveSubscriptionsByUser,
deactivateSubscription
} = require('./subscriptionStore');
const app = express();
app.use(express.json());
webPush.setVapidDetails(
'mailto:admin@company.com',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
app.post('/push/subscribe', (req, res) => {
const { userId, subscription } = req.body;
if (!userId || !subscription?.endpoint) {
return res.status(400).end();
}
saveSubscription({
userId,
endpoint: subscription.endpoint,
keys: subscription.keys,
userAgent: req.headers['user-agent']
});
res.sendStatus(201);
});
app.post('/push/send', async (req, res) => {
const { userId, title, body } = req.body;
const payload = JSON.stringify({ title, body });
const subs = await getActiveSubscriptionsByUser(userId);
await Promise.all(
subs.map(async (sub) => {
try {
await webPush.sendNotification(sub, payload);
} catch (err) {
if (err.statusCode === 404 || err.statusCode === 410) {
deactivateSubscription(sub.endpoint);
}
}
})
);
res.sendStatus(200);
});
app.listen(3000);
5. 클라이언트 코드
Service Worker
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
event.waitUntil(
self.registration.showNotification(data.title || 'Notification', {
body: data.body || ''
})
);
});
브라우저 구독 처리
<script>
(async () => {
if (!('serviceWorker' in navigator)) return;
const reg = await navigator.serviceWorker.register('/sw.js');
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
let sub = await reg.pushManager.getSubscription();
if (!sub) {
sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('VAPID_PUBLIC_KEY')
});
}
await fetch('/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 1, subscription: sub })
});
})();
function urlBase64ToUint8Array(base64) {
const raw = atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
return Uint8Array.from([...raw].map(c => c.charCodeAt(0)));
}
</script>
6. 구현 시 고려 사항
- 서버는 브라우저로 직접 메시지를 전송하지 않는다
- subscription은 브라우저 및 디바이스 단위로 관리된다
- endpoint는 고유 값으로 저장해야 한다
- Push 전송 실패(404, 410) 시 즉시 비활성화 처리한다
7. FCM과의 차이
Web Push
- 웹 표준 API 기반
- 브라우저 환경에 한정
- 구독 관리, 암호화, 실패 처리를 서버에서 직접 구현
FCM
- Google Firebase 제공 플랫폼
- Web, Android, iOS 통합 지원
- 토픽, 디바이스 그룹, 통계, 재시도 기능 제공
- Web 환경에서는 내부적으로 Web Push 사용
비교
| 구분 | Web Push | FCM |
|---|---|---|
| 성격 | 웹 표준 | 플랫폼 서비스 |
| 의존성 | 브라우저 | Firebase |
| 제어 범위 | 높음 | 제한적 |
| 구현 난이도 | 높음 | 낮음 |
| 대상 | 웹 | 웹 + 모바일 |