iOS FCM 푸시 알림 — 동작 원리부터 권한 타이밍까지 컨트롤하기

2026. 5. 26. 22:20·SWIFT개발일지

서버에서 Silent Push 기능을 추가하기로 했어요. 물론 이전에도 해봤는데, 문서화를 안 했더니 기억이 하나도 안 나더라고요 😅

 그래서 이번엔 제대로 정리해보기로 했습니다.

먼저 설정부터

1. APNs 인증 키 (.p8) 발급

Apple Developer 콘솔에서 발급해요.

Apple Developer → Certificates, Identifiers & Profiles
→ Keys → + 버튼
→ Apple Push Notifications service (APNs) 체크
→ 다운로드 (한 번만 가능, 잘 보관)

.p8 파일과 함께 Key ID, Team ID를 메모해둬야 해요. Firebase 등록할 때 필요합니다

2. Firebase 콘솔에 APNs 키 등록

여기서 파이어베이스 콘솔에서 클라우드 메시징에 들어가서 Apple 앱 구성에서 방금 다운받은것과 KeyId를 입력해주면 돼요

3. Xcode Capability 추가

Xcode → Target → Signing & Capabilities
→ + Capability
→ Push Notifications 추가
→ Background Modes 추가 → Remote notifications 체크

Push Notifications는 실제 푸시 수신을 위한 거고, Background Modes의 Remote notifications는 앱이 백그라운드에 있을 때도 Silent Push를 받기 위해 필요해요.

이렇게 세 설정이 끝나면 이제 코드로 가야죠

FCM은 어떻게 동작하는가

설정이 끝났으면, 이게 내부적으로 어떻게 동작하는지 알아야 코드를 이해할 수 있어요.

 

푸시 알림은 사실 두 레이어로 이루어진 시스템이에요

iOS 시스템 레이어  →  APNS (Apple Push Notification Service)
Firebase 레이어   →  FCM (Firebase Cloud Messaging)

흐름을 보면:

[1] 앱이 registerForRemoteNotifications() 호출
        ↓
[2] iOS ↔ APNS 서버 통신 → APNS device token 발급
        ↓
[3] Firebase SDK가 APNS token을 Firebase 서버에 등록
        ↓
[4] FCM token 생성 → didReceiveRegistrationToken 콜백으로 전달
        ↓
[5] 우리 서버에 FCM token 저장
        ↓
[6] 서버가 푸시 보낼 때 FCM token으로 Firebase API 호출
        ↓
[7] Firebase → APNS → 디바이스 배달

여기서 중요한 게 있어요.

registerForRemoteNotifications()만 호출하면 Firebase SDK가 내부적으로 APNS 등록 → FCM token 생성 → 콜백까지 다 처리해줍니다.

이게 어떻게 가능한 건지 궁금하지 않으세요?

그게 가능한 이유가 바로

 

Firebase Method Swizzling이에요

Firebase SDK를 추가하면 기본적으로 FirebaseAppDelegateProxyEnabled = true 상태예요.

Swizzling이 뭔지 먼저 알아야 해요. 

Method Swizzling은 Objective-C 런타임의 기능으로, 앱이 실행되는 시점에 특정 메서드의 구현을 다른 구현으로 교체하는 거예요.

Firebase는 앱이 시작될 때 AppDelegate의 두 메서드를 가로챕니다.

didRegisterForRemoteNotificationsWithDeviceToken
didReceiveRemoteNotification:fetchCompletionHandler:

내부적으로 이런 일이 일어나요.

앱 실행
    ↓
Firebase SDK 초기화 (FirebaseApp.configure())
    ↓
런타임에 AppDelegate의 메서드 구현을 Firebase 구현으로 교체
    ↓
이후 didRegisterForRemoteNotifications 콜백 발생 시
    → 원래 AppDelegate 구현 실행
    → Firebase 구현도 같이 실행
        → Messaging.messaging().apnsToken = deviceToken 자동 세팅

그래서 Swizzling이 ON인 상태에서는 우리가 Messaging.messaging().apnsToken = deviceToken을 직접 작성하지 않아도 Firebase가 알아서 APNS token을 받아서 FCM token 발급을 시작해요.

Swizzling을 끄는 경우

Info.plist에 이 키를 추가하면 꺼져요.

<key>FirebaseAppDelegateProxyEnabled</key>
<false/>

끄는 경우는 주로 이런 상황이에요.

  • 다른 푸시 라이브러리와 충돌할 때 
  • AppDelegate를 직접 완전히 제어해야 할 때 
  • 멀티 타겟 구조에서 예상치 못한 동작이 생길 때

Swizzling을 끄면 이 두 가지를 반드시 직접 작성해야 해요. 하나라도 빠지면 FCM token이 발급되지 않아요.

// ① APNS token → Firebase에 전달 (필수)
func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
    Messaging.messaging().apnsToken = deviceToken
}

// ② Silent Push / 백그라운드 메시지 수신 (필수)
func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
    Messaging.messaging().appDidReceiveMessage(userInfo)
    completionHandler(.newData)
}

앱 코드 흐름

권한 요청 타이밍 — 앱 시작할 때 바로 뜨면 안 되는 경우

많은 앱이 실행 직후 바로 알림 권한을 요청해요. 근데 UX 관점에서 좋지 않을 때가 있어요.

사용자가 앱을 처음 켰는데 맥락 없이 권한 요청이 뜨면 거부할 가능성이 높아요. 그래서 특정 페이지, 예를 들어 온보딩 마지막 단계나 알림이 필요한 기능 진입 시점에서 요청하는 게 낫죠.

이렇게 하려면 권한 요청과 APNS 등록을 분리해서 생각해야 해요.

// 1. 권한 요청 — 원하는 시점에 호출
func requestNotificationPermission() async {
    let granted = try? await UNUserNotificationCenter.current()
        .requestAuthorization(options: [.alert, .sound, .badge])

    if granted == true {
        await MainActor.run {
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
}

// 2. 이미 권한이 있는 사용자 — 앱 실행 시 바로 등록
func registerIfAuthorized() async {
    let settings = await UNUserNotificationCenter.current().notificationSettings()
    guard settings.authorizationStatus == .authorized else { return }

    await MainActor.run {
        UIApplication.shared.registerForRemoteNotifications()
    }
}

앱 실행 시 registerIfAuthorized()를 호출해두면, 이미 권한 있는 사용자는 자동으로 등록되고 권한 없는 사용자는 조용히 skip돼요.

권한이 없다가 설정에서 나중에 허용했을 때

iOS는 사용자가 설정에서 알림을 켜도 앱에 콜백을 주지 않아요.

설정 → 알림 → 우리 앱 → 허용 ON
→ 앱에 아무 이벤트 없음 😅

그래서 제가 선택한 방법은 ScenePhase감지였어요. 조금은 오버헤드가 있을수 있지만 

앱이 foreground로 복귀할 때마다 권한 상태를 다시 확인하는 방법을 선택했어요.

더 좋은방법이 있다면 공유부탁드립니다

흐름을 보면:

사용자: 설정에서 알림 허용 ON
    ↓
앱으로 복귀
    ↓
ScenePhase.active
    ↓
registerIfAuthorized()
    ↓
authorizationStatus == .authorized → registerForRemoteNotifications()
    ↓
APNS token 수신 → Firebase 등록 → FCM token 수신

registerIfAuthorized() 안에서 권한 상태를 매번 확인하기 때문에, 권한이 없으면 즉시 return이에요.

ScenePhase.active가 자주 호출돼도 부작용이 없어요.

정리하면 트리거는 세 곳이에요.

[1] 특정 페이지에서 권한 요청 직후
    → 신규 사용자

[2] AppDelegate.didFinishLaunching
    → 이미 권한 있는 기존 사용자

[3] ScenePhase.active
    → 설정에서 뒤늦게 권한 허용한 사용자

세 곳 모두 registerIfAuthorized() 하나만 호출해요. 어디서 시작해도 동일한 흐름을 타고, 권한 없으면 항상 안전하게 skip됩니다.

토큰 관리

FCM token을 받았으면 저장하고 서버에 전송해야 해요. 근데 단순히 받는 즉시 보내면 되는 게 아니에요.

중복 PUT 방지

Firebase는 동일한 token도 자주 재emit해요.

앱 재실행 시
lifecycle 변화 시
Firebase 내부 reconnect 시

이때마다 서버에 PUT하면 낭비예요. 이전 token과 비교해서 진짜 변경된 경우에만 PUT합니다.

실제로 서버에 업데이트 해야하는 순간은 첫 발급, 앱 재설치, 디바이스 변경, Firebase 강제 refresh가 있겠네요

race condition 주의

MessagingDelegate의 didReceiveRegistrationToken은 언제 호출될지 타이밍을 보장할 수 없어요.

구독 시작 전에 token emit이 일어날 수 있다

예를 들어 UseCase에서 token stream을 구독하기 전에 이미 delegate 콜백이 왔다면, 그 token은 그냥 날아가버려요.

그래서 가장 최근 token을 cache해두고, 구독 시작 시 즉시 emit하는 구조가 필요해요. actor로 감싸서 race condition도 방지합니다.

 

마무리

FCM을 처음 붙일 때 헷갈리는 게 "내가 token을 요청하는 건가?"인데, 그게 아니에요.

registerForRemoteNotifications()만 호출하면 Firebase SDK가 알아서 APNS 등록 → FCM token 발급 → 콜백까지 처리해줘요. 우리는 그 콜백에서 token을 받아서 저장하고 서버에 전달하면 끝이에요.

그리고 권한 요청 타이밍은 UX에 직접 영향을 주는 부분이에요. 앱 시작과 동시에 뜨는 것보다 맥락이 있는 시점에 요청하고, ScenePhase.active로 뒤늦은 권한 허용도 놓치지 않도록 하면 됩니다.

'SWIFT개발일지' 카테고리의 다른 글

TCA Dependency는 어떻게 동작하는지 정확하게 설명할수 있는 사람  (0) 2026.05.24
SwiftUI TabView 오버레이가 iOS 26에서 안 맞는 이유  (0) 2026.04.04
로컬 알림 다국어 적용기: 언어가 바뀌어도 알림을 다시 등록하지 않아도 되는 이유  (1) 2026.02.03
왜 한번 빌드 이후부터는 빌드가 더 빠를까? - 증분 빌드  (0) 2026.01.25
ProtoBuf - 이게 뭔데 사람들은 환호성을 지를까?  (1) 2025.11.29
'SWIFT개발일지' 카테고리의 다른 글
  • TCA Dependency는 어떻게 동작하는지 정확하게 설명할수 있는 사람
  • SwiftUI TabView 오버레이가 iOS 26에서 안 맞는 이유
  • 로컬 알림 다국어 적용기: 언어가 바뀌어도 알림을 다시 등록하지 않아도 되는 이유
  • 왜 한번 빌드 이후부터는 빌드가 더 빠를까? - 증분 빌드
2료일
2료일
좌충우돌 모든것을 다 정리하려고 노력하는 J가 되려고 하는 세미개발자의 블로그입니다. 편하게 보고 가세요
  • 2료일
    GPT에게서 살아남기
    2료일
  • 전체
    오늘
    어제
    • 분류 전체보기 (148)
      • SWIFT개발일지 (35)
        • ARkit (1)
        • Vapor-Server with swift (3)
        • UIkit (2)
      • 알고리즘 (25)
      • Design (6)
      • iOS (46)
        • 반응형프로그래밍 (12)
      • 디자인패턴 (6)
      • CS (3)
      • 도서관 (3)
      • 인생회고 (1)
      • AI 라이브러리 (1)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
2료일
iOS FCM 푸시 알림 — 동작 원리부터 권한 타이밍까지 컨트롤하기
상단으로

티스토리툴바