[Swift 입문] 42편 — UserNotifications와 시스템 알림 센터

🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [41편] FSEvents와 파일 시스템 감시

왜 앱 안에서 배너를 직접 그리지 않을까

사용자에게 뭔가 알려야 할 때 SwiftUI 뷰나 커스텀 윈도우로 직접 배너를 그릴 수도 있습니다. 하지만 시스템 알림 센터를 쓰면 몇 가지를 공짜로 얻습니다.

  • 앱이 백그라운드에 있거나 최소화되어 있어도 사용자에게 도달함
  • 알림 센터(화면 오른쪽 위 스와이프)에 이력이 쌓여 나중에 다시 확인 가능
  • 사용자가 시스템 설정에서 알림 스타일(배너/알림창/무음)을 직접 제어 가능
  • 액션 버튼, 사운드, 배지 숫자 등 OS가 표준화한 UI를 그대로 사용

8부에서 만든 승인 큐도 실제로는 “사용자에게 물어봐야 할 때” 알림을 띄우는 방식으로 자연스럽게 이어집니다.


권한 요청

알림도 Accessibility API(24편)나 Apple Events(40편)처럼 사용자 승인이 필요합니다. 앱 시작 시 한 번 요청합니다.

import UserNotifications

func requestNotificationPermission() {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
        if let error {
            print("알림 권한 요청 실패: \(error)")
        }
        print(granted ? "알림 허용됨" : "알림 거부됨")
    }
}

사용자가 한 번 거부하면 앱에서 다시 팝업을 띄울 수 없습니다. 시스템 설정으로 안내하는 버튼을 따로 마련해두는 것이 좋습니다.


알림 보내기

func sendNotification(title: String, body: String, identifier: String = UUID().uuidString) {
    let content = UNMutableNotificationContent()
    content.title = title
    content.body = body
    content.sound = .default

    // nil 트리거 = 즉시 발송
    let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)

    UNUserNotificationCenter.current().add(request) { error in
        if let error {
            print("알림 발송 실패: \(error)")
        }
    }
}

// 사용
sendNotification(title: "승인 필요", body: "git push origin main 실행을 승인하시겠습니까?")

triggerUNTimeIntervalNotificationTrigger를 넘기면 일정 시간 뒤에 발송하도록 예약할 수도 있습니다. identifier를 명시적으로 관리해두면, 같은 알림을 나중에 removePendingNotificationRequests(withIdentifiers:)로 취소하거나 갱신할 수 있습니다.


액션 버튼 추가하기

단순히 알리기만 하는 게 아니라, 알림 자체에서 “승인”/”거부” 버튼을 누르게 하고 싶다면 UNNotificationCategory를 등록합니다.

func registerApprovalCategory() {
    let approveAction = UNNotificationAction(
        identifier: "APPROVE_ACTION",
        title: "승인",
        options: [.authenticationRequired]  // 잠금 화면에서는 인증 요구
    )
    let denyAction = UNNotificationAction(
        identifier: "DENY_ACTION",
        title: "거부",
        options: [.destructive]
    )

    let category = UNNotificationCategory(
        identifier: "APPROVAL_REQUEST",
        actions: [approveAction, denyAction],
        intentIdentifiers: [],
        options: []
    )

    UNUserNotificationCenter.current().setNotificationCategories([category])
}

func sendApprovalNotification(command: String, approvalID: UUID) {
    let content = UNMutableNotificationContent()
    content.title = "승인 필요"
    content.body = command
    content.categoryIdentifier = "APPROVAL_REQUEST"
    content.userInfo = ["approvalID": approvalID.uuidString]

    let request = UNNotificationRequest(identifier: approvalID.uuidString, content: content, trigger: nil)
    UNUserNotificationCenter.current().add(request)
}

버튼 클릭에 반응하기

사용자가 알림의 액션 버튼을 누르면 델리게이트 메서드로 통보됩니다. 8부(39편)에서 만든 ApprovalQueue와 바로 연결할 수 있습니다.

final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
    let approvalQueue: ApprovalQueue

    init(approvalQueue: ApprovalQueue) {
        self.approvalQueue = approvalQueue
    }

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        guard let idString = response.notification.request.content.userInfo["approvalID"] as? String,
              let approvalID = UUID(uuidString: idString) else {
            completionHandler()
            return
        }

        switch response.actionIdentifier {
        case "APPROVE_ACTION":
            onUserDecision(id: approvalID, approved: true, queue: approvalQueue)
        case "DENY_ACTION":
            onUserDecision(id: approvalID, approved: false, queue: approvalQueue)
        default:
            break  // 알림 본문을 그냥 탭한 경우 등
        }

        completionHandler()
    }
}

// 앱 시작 시 델리게이트 등록
UNUserNotificationCenter.current().delegate = NotificationDelegate(approvalQueue: myApprovalQueue)

completionHandler()는 반드시 호출해야 합니다. 호출하지 않으면 시스템이 알림 처리가 끝나지 않았다고 판단해 다음 알림 전달이 지연될 수 있습니다.


포그라운드에서도 알림 보이기

기본적으로 앱이 최전면에 떠 있을 때는 배너가 뜨지 않습니다. 앱을 보고 있는 중에도 알림을 표시하려면 델리게이트에 다음 메서드를 추가합니다.

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
    completionHandler([.banner, .sound])
}

핵심 요약

  • 시스템 알림 센터는 백그라운드 도달, 이력 보관, 표준 UI를 공짜로 제공
  • UNUserNotificationCenter.requestAuthorization으로 최초 1회 권한 요청
  • UNNotificationRequest(trigger: nil)로 즉시 발송, identifier로 갱신/취소 관리
  • UNNotificationCategory + UNNotificationAction으로 알림에 승인/거부 버튼 추가
  • 버튼 클릭은 델리게이트의 didReceive로 전달 — 승인 큐(39편)와 직접 연결 가능
  • 포그라운드 배너를 보이려면 willPresent에서 completionHandler로 옵션을 지정

다음 편은 시리즈의 마지막으로, XPC와 프로세스 권한 분리를 다룹니다.

🤖 Generated with Claude Code

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다