🟣 푸시가 기기까지 오는 길
"FCM을 쓰면 APNs가 필요 없다"는 오해가 흔하다. iOS 푸시는 어떤 경우에도 APNs를 거친다. FCM은 서버에서 APNs까지의 구간을 감싸 주는 편의 레이어이고, 기기에 메시지를 꽂는 건 언제나 APNs다.
"푸시가 안 왔다"를 서버 → FCM / FCM → APNs / APNs → 기기로 나누면, FCM 대시보드만 성공인데 기기 수신이 없을 때 앱 코드부터 의심하지 않아도 된다.
🟣 앱 상태별 처리
같은 푸시라도 프로세스 상태에 따라 호출되는 API가 다르다.
| 앱 상태 | 수신 시점 | 클릭 시점 |
|---|---|---|
| Foreground | willPresent로 표시 여부 결정 | didReceive |
| Background | 시스템이 알림만 표시 (앱 코드 없음, content-available은 예외) | didReceive |
| Terminated | 시스템만 표시. 앱은 꺼져 있음 | ColdStart + launchOptions에 payload |
클릭 이후는 어느 상태에서 왔든 didReceive로 모이는 경우가 많다. Terminated만, 앱이 죽어 있는 동안의 payload를 launchOptions에서 따로 꺼내야 한다.
수신·클릭 콜백이 AppDelegate에 흩어지면 상태 분기가 복제된다. 한 핸들러 타입으로 모으고, 화면 이동은 라우터에 위임하는 편이 유지비가 낮다.
🟣 Rich Push와 Notification Service Extension
이미지가 붙은 알림은 메인 앱이 아니라 Notification Service Extension이 표시 직전에 payload를 가로채 만든다. Extension은 메인 앱과 별도 샌드박스다. UserDefaults·URLCache·Keychain(기본 그룹)이 그대로 보이지 않는다.
- payload에
"mutable-content": 1이 없으면 Extension이 에러 없이 호출되지 않는다 - 시간·메모리 한도가 있어 이미지 다운로드는 짧게, 실패 시
serviceExtensionTimeWillExpire에서 원본이라도 표시한다 - 메인 앱과 값을 공유하려면 App Group을 명시한다
🟣 ColdStart와 payload 보류
앱이 완전히 종료된 뒤 푸시를 누르면 didReceive만으로는 부족하다. application(_:didFinishLaunchingWithOptions:)의 launchOptions?[.remoteNotification]에 payload가 실려 온다.
이 시점에 내비게이션·인증이 아직 없으면 바로 라우팅하면 실패한다. URL은 일단 저장만 하고, 탭바·루트가 준비된 뒤(예: 첫 viewDidAppear)에 소비한다.
로그인 뒤에야 열 수 있는 화면이면, 로그인 완료 시점에도 같은 큐를 한 번 더 소비한다.
🟣 수신률을 믿기 어려운 이유
마케팅이 "기기 도달률"을 물을 때, Android와 iOS의 관측 범위가 다르다.
| 상태 | Android | iOS |
|---|---|---|
| Foreground | 집계 가능 | willPresent로 집계 가능 |
| Background | OS가 앱을 깨워 처리 가능 | content-available도 스로틀링으로 누락 |
| Terminated | OS가 프로세스를 깨워 수신 집계 가능 | 탭 전까지 앱·FCM이 수신을 모름 |
iOS FCM "Delivered"는 기기 실수신이 아니라 APNs 수락에 가까운 상한에 가깝다. 두 OS 숫자를 나란히 비교하면 캠페인 실패처럼 보이기 쉽다. 실무에서는 클릭률과 클릭 이후 전환처럼 양쪽에서 같은 구간만 지표로 쓰는 편이 안전하다.
🟣 용어
- APNs: Apple 푸시 게이트웨이. 기기는 APNs에 TLS로 붙어 메시지를 받는다.
- FCM: 전송 SDK. iOS 경로는 FCM → APNs → 기기.
- UNUserNotificationCenter: 권한·수신·표시·탭의 단일 진입점.
- Notification Service Extension: 표시 직전 payload 수정(Rich Push).
- ColdStart: Terminated 상태에서 푸시 탭으로 앱이 처음부터 뜨는 경우.
🟣 정리
- iOS 푸시의 마지막 구간은 항상 APNs다. FCM은 그 앞단 편의 레이어다
- Foreground / Background / Terminated마다 콜백이 다르다. Terminated는
launchOptions를 빠뜨리면 캠페인 클릭이 증발한다 - Rich Push는 Extension 샌드박스·시간 한도·
mutable-content를 같이 설계한다 - ColdStart는 "저장 후 소비"로 초기화 순서와 맞춘다
- iOS 수신률은 구조적으로 불완전하다. 클릭·전환 중심으로 지표를 잡는다
푸시로 앱이 열렸으면, 그다음 문제는 어느 화면으로 보낼지다. 다음 장은 딥링크와 앱 라우팅이다.