codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
노트›data

푸시 알림 — FCM 과 Web Push

2026-04-28 게시· 2026-05-18 갱신·0회 조회

푸시 알림 — 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 를 참고합니다.

data 카테고리의 다른 글

카테고리 전체 보기 →
  • DB 시드 소스를 코드 트리 안에 두지 않는다
  • Supabase Storage — 파일 업로드와 권한
  • Kafka 실무 — 토픽 설계와 메시지 흐름
  • 여러 PostgreSQL 풀 한 앱에서 관리하기
  • 백업과 복구
  • 이미지 파이프라인