iOS 푸시 알림 면접 2026: APNs, 토큰, 트러블슈팅

Push Notifications, APNs, 토큰 관리, 트러블슈팅에 관한 iOS 면접 준비 완벽 가이드입니다. 자주 묻는 질문에 상세한 답변을 함께 담았습니다.

iOS 푸시 알림 APNs 아키텍처와 토큰, 트러블슈팅

Push Notifications는 iOS 면접에서 여전히 핵심 주제입니다. APNs의 동작 원리를 이해하고 device token을 관리하며 흔한 문제를 해결할 수 있다는 것은 Apple 생태계에 대한 깊은 이해를 보여 줍니다. 본 가이드는 면접에서 가장 자주 등장하는 질문들을 다룹니다.

면접의 핵심 포인트

면접관은 디바이스 등록부터 알림 전달에 이르는 전 과정을 이해하고, 각 단계에서 적절히 오류를 처리할 수 있는 지원자를 찾고 있습니다.

APNs 아키텍처: iOS 알림의 토대

Apple Push Notification service (APNs)는 Apple 디바이스로 푸시 알림을 전달하는 중앙 서비스입니다. 그 아키텍처를 파악하는 것은 면접에서 효과적으로 답변하기 위한 필수 조건입니다.

APNs는 어떻게 동작합니까?

통신 흐름에는 iOS 앱, APNs, 백엔드 서버라는 세 주요 주체가 관여합니다. 전체 절차는 다음과 같습니다.

AppDelegate.swiftswift
import UIKit
import UserNotifications

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Request notification authorization
        UNUserNotificationCenter.current().requestAuthorization(
            options: [.alert, .badge, .sound]
        ) { granted, error in
            guard granted else { return }
            // Register with APNs
            DispatchQueue.main.async {
                application.registerForRemoteNotifications()
            }
        }
        return true
    }
}

APNs 등록은 두 단계로 이루어집니다. 사용자 권한을 받은 뒤 registerForRemoteNotifications()를 호출하면 됩니다.

Device token 관리

Device token은 특정 디바이스를 지목하기 위해 APNs가 생성하는 고유 식별자입니다. 이 토큰은 변경될 수 있으며, 앱이 실행될 때마다 백엔드 서버로 전송해야 합니다.

AppDelegate.swiftswift
extension AppDelegate {

    // Callback when APNs provides the token
    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        // Convert token to hexadecimal string
        let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
        print("Device Token: \(tokenString)")

        // Send to backend server
        sendTokenToServer(tokenString)
    }

    // Callback on registration failure
    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("Failed to register: \(error.localizedDescription)")
    }

    private func sendTokenToServer(_ token: String) {
        // Implement HTTP request to backend
    }
}

토큰은 Data 형태로 전달되므로, 서버로 보내기 전에 16진수 문자열로 변환해야 합니다.

자주 묻는 APNs 면접 질문

iOS 면접에서는 보통 APNs에 관한 이론적·실무적 질문이 함께 출제됩니다. 가장 흔한 질문과 상세한 답변을 정리했습니다.

질문 1: APNs sandbox와 production의 차이는 무엇입니까?

APNs 환경

APNs에는 별도의 엔드포인트를 가진 두 환경이 존재합니다. 한쪽 환경에서 발급된 토큰은 다른 환경에서는 사용할 수 없습니다.

| 환경 | 엔드포인트 | 사용처 | |------|------------|--------| | Sandbox | api.sandbox.push.apple.com | 디버그, TestFlight | | Production | api.push.apple.com | App Store |

질문 2: 토큰 만료는 어떻게 처리합니까?

Device token은 시스템 복원, 새 디바이스에서의 설치, APNs의 주기적 갱신 등 다양한 이유로 변경될 수 있습니다. 서버는 APNs의 오류 응답을 적절하게 처리해야 합니다.

NotificationService.swiftswift
enum APNsError: Int {
    case badDeviceToken = 400
    case unregistered = 410
    case payloadTooLarge = 413
    case tooManyRequests = 429
    case internalServerError = 500

    var shouldRemoveToken: Bool {
        // Remove token only if device is no longer registered
        return self == .unregistered || self == .badDeviceToken
    }
}

struct APNsResponse {
    let statusCode: Int
    let deviceToken: String

    func handleError() {
        guard let error = APNsError(rawValue: statusCode) else { return }

        if error.shouldRemoveToken {
            // Remove token from database
            TokenRepository.shared.remove(deviceToken)
        }
    }
}

서버에서의 APNs 오류 처리는 토큰 데이터베이스를 깨끗하게 유지하고 불필요한 요청을 줄이는 데 매우 중요합니다.

질문 3: silent notification은 어떻게 구현합니까?

silent notification을 활용하면 사용자에게 어떤 알림도 보여 주지 않고 백그라운드에서 앱을 깨워 작업을 수행할 수 있습니다.

AppDelegate.swiftswift
func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
    // Check if this is a silent notification
    guard let aps = userInfo["aps"] as? [String: Any],
          aps["content-available"] as? Int == 1 else {
        completionHandler(.noData)
        return
    }

    // Perform background task
    performBackgroundTask { result in
        switch result {
        case .success:
            completionHandler(.newData)
        case .failure:
            completionHandler(.failed)
        }
    }
}

silent notification의 JSON 페이로드는 aps 객체 안에 "content-available": 1을 포함해야 합니다.

iOS 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

Notification Service Extension: 고급 커스터마이징

Notification Service Extension을 사용하면 알림이 표시되기 전에 내용을 수정할 수 있습니다. 이 기능은 면접에서도 자주 다루어집니다.

Service Extension 만들기

NotificationService.swift (in Extension target)swift
import UserNotifications

class NotificationService: UNNotificationServiceExtension {

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        self.contentHandler = contentHandler
        bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

        guard let bestAttemptContent = bestAttemptContent else {
            contentHandler(request.content)
            return
        }

        // Modify content
        if let imageURLString = bestAttemptContent.userInfo["image-url"] as? String,
           let imageURL = URL(string: imageURLString) {
            downloadImage(from: imageURL) { attachment in
                if let attachment = attachment {
                    bestAttemptContent.attachments = [attachment]
                }
                contentHandler(bestAttemptContent)
            }
        } else {
            contentHandler(bestAttemptContent)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        // Called when time limit (30 seconds) is exceeded
        if let contentHandler = contentHandler,
           let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

    private func downloadImage(
        from url: URL,
        completion: @escaping (UNNotificationAttachment?) -> Void
    ) {
        URLSession.shared.downloadTask(with: url) { localURL, _, error in
            guard let localURL = localURL, error == nil else {
                completion(nil)
                return
            }

            let tempDirectory = FileManager.default.temporaryDirectory
            let fileName = url.lastPathComponent
            let destinationURL = tempDirectory.appendingPathComponent(fileName)

            try? FileManager.default.moveItem(at: localURL, to: destinationURL)

            let attachment = try? UNNotificationAttachment(
                identifier: fileName,
                url: destinationURL,
                options: nil
            )
            completion(attachment)
        }.resume()
    }
}

Extension은 내용을 수정할 수 있는 시간으로 30초를 갖습니다. 이 한도를 넘기면 serviceExtensionTimeWillExpire() 메서드가 호출됩니다.

Push Notifications 트러블슈팅

푸시 알림 디버깅은 면접에서 반복적으로 등장하는 주제입니다. 지원자는 진단에 사용되는 도구와 기법을 숙지해야 합니다.

등록 상태 확인

DebugHelper.swiftswift
struct PushNotificationDebugger {

    static func checkNotificationStatus() async {
        let center = UNUserNotificationCenter.current()
        let settings = await center.notificationSettings()

        print("=== Push Notification Status ===")
        print("Authorization: \(settings.authorizationStatus.description)")
        print("Alert: \(settings.alertSetting.description)")
        print("Badge: \(settings.badgeSetting.description)")
        print("Sound: \(settings.soundSetting.description)")
        print("Notification Center: \(settings.notificationCenterSetting.description)")
    }
}

extension UNAuthorizationStatus {
    var description: String {
        switch self {
        case .notDetermined: return "Not Determined"
        case .denied: return "Denied"
        case .authorized: return "Authorized"
        case .provisional: return "Provisional"
        case .ephemeral: return "Ephemeral"
        @unknown default: return "Unknown"
        }
    }
}

이 디버그 함수를 사용하면 알림 권한 상태를 빠르게 확인할 수 있습니다.

자주 발생하는 오류와 해결책

꼭 알아야 할 대표 오류

면접관은 흔한 오류와 해결 방법을 자주 묻습니다. 다음 표가 반드시 기억해야 할 핵심 내용입니다.

| 오류 | 원인 | 해결책 | |------|------|--------| | 토큰 무효 | 환경 불일치 (sandbox/prod) | provisioning profile 확인 | | 알림 미수신 | 저전력 모드 활성화 | 충전된 상태에서 재시도 | | Extension 미호출 | 페이로드에 mutable-content 부재 | "mutable-content": 1 추가 | | Background fetch 실패 | 사용자가 앱 종료 | 사용자에게 제한 사항 안내 |

APNs를 직접 테스트

개발 단계에서 알림을 테스트할 때는 curl 도구로 APNs에 직접 요청을 보낼 수 있습니다.

TestPayload.swiftswift
struct APNsTestPayload {

    static let silentNotification = """
    {
        "aps": {
            "content-available": 1
        },
        "custom-data": "test"
    }
    """

    static let richNotification = """
    {
        "aps": {
            "alert": {
                "title": "New message",
                "body": "Message content"
            },
            "mutable-content": 1,
            "sound": "default"
        },
        "image-url": "https://example.com/image.jpg"
    }
    """
}

위와 같은 테스트 페이로드는 다양한 알림 유형에서 앱이 어떻게 동작하는지 검증하는 데 유용합니다.

운영 환경 모범 사례

모범 사례에 관한 질문은 지원자가 운영 중인 앱을 어떻게 다루어 왔는지 평가하는 데 도움이 됩니다.

네트워크 오류 처리

PushTokenManager.swiftswift
actor PushTokenManager {

    private var pendingToken: String?
    private var retryCount = 0
    private let maxRetries = 3

    func registerToken(_ token: String) async {
        pendingToken = token
        await sendTokenWithRetry()
    }

    private func sendTokenWithRetry() async {
        guard let token = pendingToken else { return }

        do {
            try await APIClient.shared.registerPushToken(token)
            pendingToken = nil
            retryCount = 0
        } catch {
            retryCount += 1
            if retryCount < maxRetries {
                // Retry with exponential backoff
                let delay = UInt64(pow(2.0, Double(retryCount))) * 1_000_000_000
                try? await Task.sleep(nanoseconds: delay)
                await sendTokenWithRetry()
            }
        }
    }
}

actor를 사용하면 동시성 환경에서 토큰을 다룰 때 스레드 안전성을 확보할 수 있습니다.

토큰의 로컬 저장

TokenStorage.swiftswift
struct TokenStorage {

    private static let tokenKey = "com.app.pushToken"

    static func save(_ token: String) {
        UserDefaults.standard.set(token, forKey: tokenKey)
    }

    static func retrieve() -> String? {
        UserDefaults.standard.string(forKey: tokenKey)
    }

    static func hasTokenChanged(_ newToken: String) -> Bool {
        guard let savedToken = retrieve() else { return true }
        return savedToken != newToken
    }
}

토큰을 로컬에 저장해 두면 토큰이 바뀌지 않은 경우 불필요한 네트워크 호출을 막을 수 있습니다.

결론

iOS Push Notifications를 능숙하게 다룰 수 있다는 점은 Apple 생태계와 클라이언트-서버 상호작용을 깊이 이해하고 있음을 보여 줍니다. 면접에서 기억해 두어야 할 핵심 포인트는 다음과 같습니다.

✅ APNs 아키텍처: 등록부터 전달까지의 전체 흐름을 이해할 것

✅ Device token: 라이프사이클과 변경 시 대응 방식

✅ Notification Service Extension: 30초 한도 내에서의 콘텐츠 가공

✅ 트러블슈팅: 자주 발생하는 오류와 해결책 숙지

✅ silent notifications: background fetch를 위한 content-available

✅ 모범 사례: 재시도 로직, 로컬 저장, 오류 처리

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#ios
#push-notifications
#apns
#swift
#interview

공유

관련 기사