//  .swift
//  ogging.dev / Swift
//  JAN 10, 2022 · 5 min read
//  #fcm, #firebase, #pushnotification

iOS에 FCM 붙이기 — 인증 키부터 토큰 흐름, 그리고 자주 막히는 지점

Firebase Cloud Messaging을 iOS 프로젝트에 도입할 때 인증 키 발급부터 토큰 수신, 페이로드 수신, 자주 막히는 함정까지 한 번에 정리.

iOS에 푸시를 붙일 때 가장 흔히 쓰는 게 Firebase Cloud Messaging(FCM)이다. Google이 APNs 위에 한 겹 추상화를 깔아둔 형태라, 서버에서 한 가지 페이로드 포맷으로 iOS/Android/웹에 보낼 수 있어서 편하다. 다만 처음 붙일 때 인증 키, capability, 토큰 흐름 같은 게 한 번에 묶여서 어디서 막히는지 헷갈리기 쉽다. 한 번 정리해 둔다.

1. 사전 준비

세 가지가 필요하다.

  1. Apple Developer 푸시 인증 키 (.p8) — Apple Developer Portal → Keys → APNs 권한 켜고 생성.
  2. Xcode 프로젝트의 Push Notifications capability — Signing & Capabilities에서 추가.
  3. Firebase 프로젝트 — Firebase Console에서 새 프로젝트 → iOS 앱 등록 → Bundle ID 입력 → GoogleService-Info.plist 다운.

인증서(.p12)도 여전히 쓸 수 있지만, Apple도 Firebase도 .p8 Auth Key를 권장한다. 키 하나면 만료 없이 여러 앱에 재사용 가능해서 관리가 훨씬 편하다.

2. SDK 설치

Swift Package Manager 기준.

https://github.com/firebase/firebase-ios-sdk

필요한 product만 골라 넣으면 되는데, FCM에는 보통 두 개만 있으면 충분.

  • FirebaseMessaging
  • FirebaseAnalytics (선택, FCM 일부 통계가 Analytics에 의존)

3. 초기화

GoogleService-Info.plist를 프로젝트에 추가했으면, AppDelegate에서 한 줄.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import UIKit
import FirebaseCore
import FirebaseMessaging
import UserNotifications

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        FirebaseApp.configure()

        Messaging.messaging().delegate = self
        UNUserNotificationCenter.current().delegate = self

        UNUserNotificationCenter.current().requestAuthorization(
            options: [.alert, .badge, .sound]
        ) { granted, _ in
            print("notification authorization:", granted)
        }
        application.registerForRemoteNotifications()
        return true
    }
}

SwiftUI 라이프사이클(@main struct App)을 쓰는 프로젝트라면 UIApplicationDelegateAdaptor로 AppDelegate를 끼워 넣으면 같은 코드가 동작한다.

1
2
3
4
5
@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene { WindowGroup { RootView() } }
}

4. APNs 토큰 ↔ FCM 토큰 연결

FCM은 내부적으로 APNs 토큰을 받아서 자기 FCM 토큰으로 매핑한다. iOS는 두 곳에서 토큰을 받는다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
extension AppDelegate {
    // APNs가 발급한 디바이스 토큰
    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        Messaging.messaging().apnsToken = deviceToken
    }

    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("APNs 등록 실패:", error)
    }
}

extension AppDelegate: MessagingDelegate {
    // FCM이 발급/갱신한 토큰
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        guard let fcmToken else { return }
        // 서버에 등록
        AlarmAPI.shared.register(token: fcmToken)
    }
}

FCM 토큰은 앱 첫 실행 / 재설치 / 데이터 초기화 / 토큰 갱신 시에 바뀐다. delegate가 호출될 때마다 서버에 보내는 게 가장 안전.

또는 명시적으로 가져오는 방법도 있다.

1
2
3
4
Messaging.messaging().token { token, error in
    if let error { print("token error:", error); return }
    print("FCM token:", token ?? "")
}

5. Firebase Console에서 APNs 인증 키 업로드

이걸 안 하면 토큰은 받아져도 실제 푸시가 안 온다. 가장 흔히 막히는 지점.

Firebase Console → 프로젝트 설정 → Cloud Messaging 탭 → “Apple 앱 구성” 섹션 → APNs 인증 키 업로드.

업로드 시 필요한 항목:

  • .p8 키 파일
  • Key ID (Apple Developer Portal에서 확인)
  • Team ID

6. 페이로드 수신

포그라운드와 백그라운드 처리가 분리돼 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extension AppDelegate: UNUserNotificationCenterDelegate {
    // 포그라운드 — 시스템 배너를 띄울지 결정
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        let userInfo = notification.request.content.userInfo
        print("foreground payload:", userInfo)
        completionHandler([.banner, .sound, .badge])
    }

    // 배너 탭 후 진입
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        let userInfo = response.notification.request.content.userInfo
        // 딥링크 라우팅 등
        completionHandler()
    }
}

userInfo에는 서버가 보낸 커스텀 키가 들어 있다. 딥링크 URL이나 라우팅 식별자를 여기에 실어 두고, 탭 시 화면 이동에 활용.

7. 자주 막히는 지점

푸시 붙이면서 가장 자주 만나는 함정들.

  • 시뮬레이터로 푸시 테스트 → 안 온다. 옛 시뮬에선 아예 불가능했고, 지금은 가능하긴 한데 동작이 미묘하다. 실제 기기로 테스트하는 게 가장 빠르다.
  • Messaging.messaging().delegate = self를 안 함. didReceiveRegistrationToken이 영원히 안 불려서 “토큰이 왜 안 오지?” 하고 헤맴.
  • Push Notifications capability를 안 켬. 콘솔에서 인증 키까지 다 잘 박았는데 푸시가 안 오는 1순위 이유.
  • Background Modes의 “Remote notifications” — silent push(content-available: 1) 받을 거라면 켜야 함. 일반 알림만 받을 거면 필수는 아님.
  • Provisioning Profile에 push entitlement 누락. 자동 서명 쓰면 보통 알아서 들어가는데, 수동 서명 환경에선 capability 추가 후 프로파일 재생성 필요.
  • APNs 환경 불일치 — Development 인증 키로 빌드해놓고 TestFlight에서 안 오는 경우. aps-environment가 development인지 production인지 확인.

8. 페이로드 예시

서버에서 FCM HTTP v1 API로 쏘는 페이로드 예.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{
  "message": {
    "token": "DEVICE_FCM_TOKEN",
    "notification": {
      "title": "주문이 접수됐어요",
      "body": "조금만 기다려 주세요"
    },
    "data": {
      "deeplink": "myapp://orders/12345"
    },
    "apns": {
      "payload": {
        "aps": {
          "sound": "default",
          "badge": 3
        }
      }
    }
  }
}

notification은 OS가 알아서 배너로 띄우는 표준 영역, data는 앱이 해석하는 커스텀 영역. 둘 다 보낼 수 있고, 보통은 둘 다 채워서 보낸다.

정리

  • 인증 키(.p8) → SDK 설치 → AppDelegate 초기화 → APNs 토큰을 FCM에 전달 → Firebase Console에 인증 키 업로드 → 페이로드 수신 핸들러. 이 흐름만 한 번 깔끔하게 잡으면 끝.
  • 거의 모든 푸시 안 됨 원인은 capability 누락, delegate 미설정, Console 인증 키 누락 셋 중 하나.
  • 진짜로 동작하는지 확인할 땐 반드시 실제 기기로.

자세한 공식 가이드는 Firebase 문서에 잘 정리돼 있다. 한 번 붙여 두면 그 뒤로는 토큰 갱신/페이로드 처리 정도만 계속 손보면 된다.