푸시 알림 — FCM 과 Web Push
푸시 알림 — FCM 과 Web Push
모바일·웹에서 사용자가 앱을 열고 있지 않아도 메시지를 보내려면 운영체제·브라우저가 제공하는 푸시 채널을 통과해야 합니다. iOS 는 APNs, Android 는 FCM, 웹은 Web Push 표준이 그 자리에 있습니다.
1. FCM 에 대한 이야기
FCM (Firebase Cloud Messaging) 은 Google 의 푸시 메시징 서비스입니다. GCM (Google Cloud Messaging, 2012) 의 후속입니다. 2016 년 FCM 으로 브랜드 통합됐고 이후 GCM API 는 단계적으로 종료 (2018 ~ 2024) 됐습니다. 현재 권장되는 API 는 HTTP v1 입니다 (레거시 server key API 는 deprecated).
FCM 은 다음 채널을 묶습니다.
- Android — 자체 채널.
- iOS — 내부적으로 APNs 를 통해 전달.
- Web — 표준 Web Push 위에서 동작.
2. APNs 와 Web Push
APNs (Apple Push Notification service) — Apple 의 자체 채널 (2009 도입). iOS · iPadOS · macOS · watchOS · tvOS 에 메시지를 전달합니다. 인증은 토큰 기반 (JWT) 또는 인증서 기반.
Web Push — W3C · IETF 가 표준화한 브라우저 푸시. RFC 8030 (HTTP Web Push, 2016) · RFC 8291 (메시지 암호화, 2017) · RFC 8292 (VAPID, 2017). VAPID 는 발신자가 자기 신원을 푸시 서비스에 증명하는 메커니즘입니다. Chrome · Firefox · Edge · Safari 가 지원합니다 (Safari 는 16.4 / 2023 부터 macOS · iOS 16.4+).
3. 토큰 라이프사이클
① 요청 클라이언트가 푸시 권한을 사용자에게 요청
② 발급 OS · 브라우저가 토큰(또는 subscription) 발급. 앱·기기 별 고유.
③ 전달 클라이언트가 자기 백엔드에 토큰 등록
④ 사용 백엔드가 푸시 서비스에 토큰 + 메시지 페이로드 제출
⑤ 갱신 OS 가 임의로 토큰을 무효화·재발급할 수 있음
⑥ 만료 사용자가 권한을 끄거나 앱이 제거되면 토큰이 살아있어도 실패 누적
토큰은 영원하지 않습니다. 갱신·정리 흐름이 필요합니다.
4. 메시지 종류 (FCM 기준)
| 종류 | 동작 |
|---|---|
| notification | 시스템 트레이에 자동 표시. 앱이 백그라운드일 때 OS 가 처리. |
| data | 페이로드만 전달. 앱이 직접 처리. 백그라운드 처리 한계가 OS 별로 다름. |
| combined (notification + data) | 두 정보 모두. 백그라운드는 OS 가, 포그라운드는 앱이 처리. |
iOS 의 백그라운드 data-only 메시지는 content-available: 1 같은 추가 설정과 OS 의 처리 우선순위 가정에 영향을 받습니다. 신뢰성은 보장되지 않는 경우가 자주 보고됩니다.
5. 페이로드와 firebase-admin
FCM HTTP v1 페이로드 예시:
{
"message": {
"token": "<device_token>",
"notification": {
"title": "새 메시지",
"body": "확인해 주세요"
},
"data": {
"type": "chat",
"chat_id": "1234"
},
"android": { "priority": "high" },
"apns": {
"payload": {
"aps": { "sound": "default" }
}
}
}
}
Node · Python · Java · Go · .NET SDK 가 있습니다.
import { initializeApp, cert } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';
initializeApp({ credential: cert(serviceAccountJson) });
await getMessaging().send({
token,
notification: { title, body },
data: { type: 'chat', chat_id },
});
서비스 계정 JSON 키는 시크릿입니다. 환경 변수 또는 시크릿 매니저로 관리합니다. SDK 가 OAuth2 토큰 갱신·HTTP v1 호출을 추상화합니다.
6. 자체 Web Push 서버
VAPID 키 (공개키·비공개키 쌍) 를 만들고 클라이언트는 pushManager.subscribe(...) 로 subscription 을 받습니다. 서버는 web-push (Node) · pywebpush (Python) 등의 라이브러리로 직접 푸시 서비스 (Firefox autopush · Chrome FCM 엔드포인트) 에 호출합니다. 페이로드는 ECE (RFC 8188) 로 암호화됩니다.
이 경로는 Firebase 가입 없이도 가능합니다. iOS Web Push (16.4+) 도 같은 표준을 따릅니다.
7. OneSignal 과 비교
OneSignal 은 푸시·이메일·SMS 를 묶은 매니지드 서비스 (2014) 입니다. FCM · APNs · Web Push 를 한 SDK 로 추상화합니다. 분석·세그먼테이션·A/B 가 강점으로 자주 거론됩니다. 한계는 사용자 데이터가 외부에 저장된다는 점, 무료 티어 한도입니다.
| 항목 | FCM (직접) | OneSignal | 자체 Web Push |
|---|---|---|---|
| 모바일 (iOS+Android) | 가능 (firebase-admin) | 가능 | iOS는 16.4+ 웹만 |
| 웹 | 가능 (FCM JS SDK) | 가능 | 가능 (VAPID) |
| 분석·세그먼테이션 | 기본 한정 | 강함 | 직접 구현 |
| 데이터 소유 | Google 인프라 경유 | OneSignal 경유 | 자체 + 푸시 서비스 |
| 비용 | 사용량 기반 (Google) | 티어 | 매우 저렴 |
8. 토큰 정리와 토픽
서버는 발송 결과를 받아 실패한 토큰을 정리합니다.
- FCM:
messaging/registration-token-not-registered같은 에러 → 토큰 행 삭제. - Web Push:
410 Gone또는404 Not Found→ subscription 삭제.
FCM 은 토픽 구독 모델을 제공합니다 (/topics/news). 한 번의 호출로 같은 토픽의 모든 구독자에게 보낼 수 있습니다. 다만 토픽은 사용자 수가 늘면 응답 지연이 보고됩니다. 정확한 분산이 필요하면 직접 토큰 묶음으로 보내는 batch · multicast 가 자주 쓰입니다.
9. 서버 측 토큰 저장과 정리
발송 서버는 클라이언트가 등록한 토큰을 어딘가에 보관해야 발송 시점에 꺼내 쓸 수 있습니다. 토큰은 사용자 단위가 아니라 기기 단위라는 점이 저장 설계의 출발점입니다.
자주 쓰는 형태는 사용자 식별자를 키에 넣은 Redis 항목입니다.
notif:token:<userId> → Set 또는 Hash (기기별 토큰 모음)
- sliding TTL — 토큰을 쓰거나 갱신할 때마다 TTL 을 다시 늘립니다 (예: 60일). 일정 기간 앱을 열지 않은 사용자의 토큰은 자연히 만료돼 사라집니다. 별도 정리 배치 없이 비활성 사용자가 걸러집니다.
- 발송 성공 후 일회성 소비 — 일회성 알림(가입 환영, 비밀번호 변경 통지 등)은 발송에 성공하면 해당 토큰 항목을 즉시 삭제합니다. 재시도 로직이 같은 알림을 두 번 집어 보내는 사고를 막습니다. 반복 알림(채팅·뉴스)은 소비하지 않고 유지합니다.
- Redis 장애 대비 DB 이중 저장 — Redis 는 빠르지만 휘발성입니다. 토큰을 영구 테이블에도 함께 적재하면, Redis 가 비워져도 DB 에서 복구할 수 있습니다. Redis 는 조회 캐시, DB 는 원본(SSOT) 으로 둡니다.
조회 흐름: Redis HIT → 사용
Redis MISS → DB 조회 → Redis 채움 → 사용
토큰 저장은 "한 번 넣고 잊는" 데이터가 아닙니다. 갱신될 때 옛 토큰을 지우고 새 토큰을 넣는 흐름이 빠지면 한 기기에 토큰이 중복 적재돼 같은 알림이 두 번 갑니다.
10. 발송 실패 코드별 처리
푸시 서비스는 발송 결과를 에러 코드로 돌려줍니다. 모든 실패를 똑같이 재시도하면 안 되는 토큰을 계속 두드리거나, 살릴 수 있는 발송을 버립니다. 코드별로 갈래를 나눕니다.
| FCM 에러 코드 | 의미 | 처리 |
|---|---|---|
UNREGISTERED / registration-token-not-registered |
앱 삭제·토큰 만료. 더 이상 유효하지 않음 | 토큰 영구 삭제. 재시도 금지 |
INVALID_ARGUMENT |
payload 형식 오류 (필드명·크기·타입) | 재시도 금지. 같은 요청은 계속 실패 — 코드를 고쳐야 함 |
QUOTA_EXCEEDED |
전송 한도 초과 | 지수 백오프 재시도. 간격을 늘려가며 다시 시도 |
UNAVAILABLE / INTERNAL |
푸시 서버 일시 장애 | 지수 백오프 재시도. 보통 잠시 후 회복 |
SENDER_ID_MISMATCH |
설정 오류 — 토큰을 발급한 클라이언트 앱과 서버 자격증명이 서로 다른 Firebase 프로젝트 | Sender ID·서비스 계정 짝을 먼저 바로잡고, 그 다음 토큰 정리 |
핵심 갈래는 네 가지입니다.
- 영구 실패 → 토큰 삭제 —
UNREGISTERED가 대표. 죽은 토큰을 방치하면 발송 통계가 망가지고 호출 한도를 낭비합니다. - 요청 자체 오류 → 재시도 금지 —
INVALID_ARGUMENT는 네트워크 문제가 아니라 코드 문제입니다. 재시도해도 무한 실패하므로 로그를 남기고 멈춥니다. - 설정 오류 → 환경부터 점검 —
SENDER_ID_MISMATCH는 토큰이 아니라 설정 문제입니다. 클라이언트 앱과 서버 자격증명(서비스 계정)이 가리키는 Firebase 프로젝트가 다른 것이므로, 토큰부터 지우면 재등록된 토큰도 같은 불일치로 다시 실패합니다. Sender ID·서비스 계정 짝을 먼저 바로잡습니다. - 일시 장애 → 지수 백오프 —
QUOTA_EXCEEDED·UNAVAILABLE은 시간이 해결합니다. 1초 → 2초 → 4초 식으로 간격을 늘리며 한도까지만 재시도하고, 초과하면 포기합니다.
Web Push 도 같은 원리입니다. 404 · 410 Gone 은 영구(subscription 삭제), 429 · 5xx 는 일시(재시도) 로 나눕니다.
11. 멀티 디바이스와 배치 발송
한 사용자가 폰·태블릿·PC 웹을 함께 쓰면 한 사용자에 토큰이 여러 개입니다. 발송은 사용자 단위로 시작하되, 실제 전송은 그 사용자의 모든 토큰에 각각 나갑니다.
사용자 1명 → 토큰 3개 (폰 · 태블릿 · 웹)
→ 3건 발송 · 결과도 3건 (토큰별로 다를 수 있음)
대량 발송에는 멀티캐스트가 쓰입니다.
sendMulticast(또는sendEach) — 토큰 묶음을 한 번의 호출로 보냅니다. FCM 은 한 호출에 최대 500개 토큰 제한이 있습니다. 그보다 많으면 500개씩 잘라 여러 번 호출합니다.- 응답은 토큰별 배열 — 500개를 보내면 성공·실패가 섞인 결과 배열이 옵니다. 실패한 인덱스의 토큰만 골라 §10 의 코드별 처리(영구 실패는 삭제)를 적용합니다.
// 500개 단위로 잘라 발송
const CHUNK = 500;
for (let i = 0; i < tokens.length; i += CHUNK) {
const batch = tokens.slice(i, i + CHUNK);
const res = await getMessaging().sendEachForMulticast({
tokens: batch,
notification: { title, body },
});
res.responses.forEach((r, idx) => {
if (!r.success && isUnregistered(r.error)) {
deleteToken(batch[idx]); // 죽은 토큰 정리
}
});
}
발송은 요청 스레드를 점유하지 않습니다. 사용자의 HTTP 요청을 처리하는 도중에 수천 건의 푸시를 동기로 보내면 응답이 그만큼 늦어집니다. 발송 작업은 큐에 넣거나(메시지 큐·작업 큐), 별도 비동기 실행(예: Spring @Async, Node 의 백그라운드 워커)으로 분리합니다. 요청은 "발송 접수됨"만 빠르게 응답하고, 실제 전송은 뒤에서 진행합니다.
HTTP 요청 → 발송 작업 큐에 적재 → 즉시 202 응답
↓
워커가 큐에서 꺼내 멀티캐스트 발송 · 실패 토큰 정리
12. 자주 걸리는 자리
레거시 server key API 사용 — HTTP v1 으로 이미 옮겼는지 확인합니다. 옛 GCM/FCM legacy API 는 종료된 자리입니다.
iOS data-only 의 신뢰성 — 백그라운드 데이터 전용 메시지는 OS 의 처리 정책에 흔들립니다. 표시가 필요한 알림은 notification 페이로드도 포함합니다.
서비스 워커 누락 — Web Push 는 등록된 서비스 워커가 메시지를 받습니다. PWA 가 아니어도 서비스 워커가 필요합니다.
VAPID 키 분실 — 자체 Web Push 의 키를 잃으면 기존 subscription 이 무효가 됩니다.
앱 종료 후 알림 — Android 의 일부 OEM 은 강제 종료된 앱의 백그라운드 처리를 막아 알림이 늦거나 누락될 수 있습니다.
페이로드 한도 — APNs · FCM 모두 페이로드 크기 제한 (약 4KB) 이 있습니다. 큰 데이터는 토큰만 보내고 클라이언트가 별도 API 호출합니다.
시간대 가정 — 단순 발송이 사용자 시간대를 무시하면 새벽 알림이 됩니다.
하고픈 말
푸시는 사용자 동의가 가장 어려운 단계입니다. 동의 직후 처음 받는 알림이 가치 있게 느껴지는지가 옵트아웃 비율을 결정합니다. 새벽 알림·과도한 빈도는 한 번에 권한이 닫히는 자리입니다.
Next
- image-pipeline
- backup-restore
FCM 공식 문서 · FCM HTTP v1 · APNs 공식 · RFC 8030 — HTTP Web Push · web-push npm · MDN Push API 를 참고합니다.