--- id: ios-push-notifications title: iOS Push Notifications — APNs / 권한 / 백그라운드 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [ios, push, apns, notifications, vibe-coding] tech_stack: { language: "Swift / APNs", applicable_to: ["iOS"] } applied_in: [] aliases: [APNs, device token, silent push, rich notification] --- # iOS Push Notifications > Apple Push Notification service (APNs) 통한 push. **(1) 권한 요청, (2) device token 받기 → 서버 등록, (3) APNs payload, (4) 받은 후 처리** 4단계. silent push 와 alert push 다름. ## 📖 핵심 개념 - Alert push: UI 표시 (사용자 권한 필요). - Silent push: 백그라운드 데이터 sync — UI 없음, 권한 X. `content-available: 1`. - 사용자 행동 (탭, action) 처리 별도. ## 💻 코드 패턴 ### 권한 요청 ```swift import UserNotifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .provisional]) { granted, error in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } ``` `provisional`: 사용자 명시 동의 없이 silently 등록 → 알림이 처음엔 조용히 노티 센터에만. 좋은 UX. ### Device token 받기 (AppDelegate) ```swift func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() Task { try? await api.registerPushToken(token) } } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("APNs reg failed: \(error)") } ``` ### Foreground / background 표시 ```swift // AppDelegate / scene UNUserNotificationCenter.current().delegate = self func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .badge, .sound]) // foreground 에서도 banner } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo if let url = userInfo["deeplink"] as? String { open(url) } completionHandler() } ``` ### Silent push 처리 (background fetch) ```swift func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult { do { try await syncManager.pull() return .newData } catch { return .failed } } ``` `Info.plist` → Background Modes → Remote notifications enable. ### APNs payload 예시 ```json { "aps": { "alert": { "title": "새 메시지", "body": "Alice: 안녕" }, "badge": 3, "sound": "default", "thread-id": "chat-1", "category": "MESSAGE" }, "deeplink": "myapp://chat/1" } ``` Silent: ```json { "aps": { "content-available": 1 }, "syncToken": "abc" } ``` ## 🤔 의사결정 기준 | 알림 종류 | 사용 | |---|---| | 사용자 메시지 (chat, mention) | Alert (소리 + 진동) | | 백그라운드 sync (이메일, 캘린더) | Silent (content-available) | | 부드러운 안내 (cron 마케팅) | Provisional (조용한 노티) | | 시간 민감 (alarm, 결제 인증) | Time-Sensitive interrupt level | | 위치 변화 / 트리거 | Location notification | ## ❌ 안티패턴 - **권한 요청을 앱 첫 실행에 즉시**: 거부율 높음. 가치 보여준 후 적절 시점. - **device token 한 번만 등록**: token 은 변경 가능 (앱 재설치, OS 업그레이드). 매 launch 등록 + 서버 dedup. - **token 을 분석/로그에 남김**: PII 비슷. redact. - **silent push 폭주**: APNs 가 throttle. 10/hour 정도가 안전. - **백그라운드 fetch 가 너무 무거움**: 30초 한도. 가벼운 작업만, 큰 sync 는 BGTask. - **iOS Foreground 에서 알림 안 보여줌 가정**: willPresent 안 구현하면 그냥 사라짐. - **notification action 안 처리**: 사용자 답장 / 액션 버튼 무반응. ## 🤖 LLM 활용 힌트 - 권한 = provisional 우선, 명시 권한은 가치 노출 후. - Silent 는 조심 (throttle). ## 🔗 관련 문서 - [[iOS_Background_Tasks]] - [[Backend_Job_Queue_Patterns]]