iOS/Swift

[Foundation/Class] NotificationCenter

태애니 2025. 7. 2. 00:10
728x90

NotificationCenter 란?

  • 객체 간 느슨한 통신(loose coupling)을 가능하게 하는 event broadcast system
  • 다수의 객체에 이벤트 상태변화를 알리는 방송이라고 생각하면 됨
  • 단일 프로그램 내에서만 동작 (프로세스 간 통신은 DistributedNotificationCenter 사용)

Notification 이란?

  • NotificationCenter가 전달하는 단일 이벤트의 데이터 단위
  • 앱 내부의 객체 간 메시지를 전달하는 역할로, 옵저버 패턴의 일부이다.

[!NOTE] Notification과 User Notification(UNNotification)은 다른 개념임.

NotificationCenter의 내부구조

 
swift
class NotificationCenter {
    private var observers: [NotificationName: [WeakObserver]] = [:]
    private let lock = NSLock() // 스레드 안전성
    
    // 실제 내부 구조 (단순화)
    private struct WeakObserver {
        weak var observer: AnyObject?
        let selector: Selector?
        let block: ((Notification) -> Void)?
        let queue: OperationQueue?
    }
}

Notification vs User Notification 비교

항목NotificationUser Notification (UNNotification)

소속 프레임워크 Foundation (NotificationCenter) UserNotifications (UNUserNotificationCenter)
주요 클래스 Notification, NotificationCenter UNNotification, UNUserNotificationCenter
이벤트 대상 앱 내부 객체 (코드) 사용자 (시스템 알림 UI)
발생 위치 앱 내부에서 직접 post 시스템이 푸시/로컬 알림을 표시할 때
용도 코드 간 통신 (모듈 간, VC ↔ VM 등) 사용자에게 알림을 보내고 응답 처리
인터넷 필요 여부 필요없음 로컬일 경우 필요없음, 원격일 경우 필요
 
swift
// UserNotification을 이용해 알림을 하면서, 처리 후 내부 Notification으로 전달하는 예시
// 푸시 알림 후 앱 내부에서 이벤트를 처리하는 예시임
// 사용자가 알림을 탭했을 때 → 앱 내부에 Notification 전달
func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    NotificationCenter.default.post(name: .didTapReminderNotification, object: nil, userInfo: response.notification.request.content.userInfo)
}

GQ1. NotificationCenter의 처리 과정

1. 관찰자 등록 (addObserver)

옵저버 객체는 특정 Notification.Name에 대해 알림을 수신하겠다고 등록

2. 알림 게시 (post)

알림 이름, 옵셔널한 발신자(sender), userInfo를 포함한 알림을 게시

3. 알림 전달

등록된 모든 observer에게 동기적으로 알림이 전달 옵저버는 등록 시 설정한 selector/closure를 통해 콜백을 받음

 
swift
// 셀렉터 기반 등록
NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleKeyboardWillShow),
    name: UIResponder.keyboardWillShowNotification,
    object: nil
)

// 클로저 기반 등록
let observer = NotificationCenter.default.addObserver(
    forName: .customEvent,
    object: nil,
    queue: .main
) { [weak self] notification in
    self?.handleCustomEvent(notification)
}

처리 순서 상세

  1. 등록 단계: 옵저버가 특정 알림 이름에 대해 콜백 등록
  2. 발송 단계: post 메서드 호출 시 해당 알림 이름의 모든 옵저버 검색
  3. 전달 단계: 등록된 순서대로 각 옵저버의 콜백 실행 (동기적)
  4. 완료 단계: 모든 옵저버 처리 완료 후 post 메서드 반환

Observer Pattern

옵저버 패턴은 한 객체의 상태 변화를 감지 후, 다른 객체들이 자동으로 반응하도록 연결하는 패턴

 어떤 일이 발생했을 때, 관심있는 객체들에게 자동으로 알려주는 구조

  • 변화가 발생하면 자동으로 반응하게 하는 시스템
  • 여러 객체가 한 이벤트에 대해 동시에 반응한다 (1:N)

옵저버 패턴 구현 예시

 
// Subject의 상태가 바뀔 때마다, 모든 Observer가 자동으로 반응함
protocol Observer {
    func update(value: Int)
}

class Subject {
    private var observers = [Observer]()
    private var value = 0

    func addObserver(_ observer: Observer) {
        observers.append(observer)
    }

    func changeValue(to newValue: Int) {
        value = newValue
        notifyObservers()
    }

    private func notifyObservers() {
        observers.forEach { $0.update(value: value) }
    }
}

NotificationCenter로 보는 옵저버 패턴

  • NotificationCenter 자체가 Subject
  • ViewController, ViewModel = Observer
  • Notification = Event
 
swift
NotificationCenter.default.addObserver(self, selector: #selector(...), name: .eventHappened, object: nil)

옵저버 패턴의 장점

  • 느슨한 결합: 주체와 반응하는 객체가 서로 모른 채로 동작
  • 유연성: 여러 객체가 같은 이벤트에 반응 가능 (1:N 구조)
  • 확장성: 옵저버를 자유롭게 추가/제거 가능

옵저버 패턴의 단점

  • 순환 참조 위험: 클로저 내부에서 self를 강하게 참조할 경우 → 해결방안 [weak self] 사용
  • 메모리 누수 가능성: 옵저버 제거 누락 시 → deinit에서 removeObserver 호출 처리
  • 디버깅 어려움: 이벤트 흐름 추적이 복잡 → 네이밍 전략, 구조화 필요

GQ2. NotificationCenter의 메모리 관리

중요한 수정사항 

iOS 9+ 부터는 셀렉터 기반 옵저버도 자동으로 해제된다.

 
// iOS 9+ 에서는 자동 해제됨
NotificationCenter.default.addObserver(self, selector: #selector(handle), name: .custom, object: nil)

// deinit에서 수동 제거 불필요 (하지만 명시적으로 하는 것이 좋음)
deinit {
    NotificationCenter.default.removeObserver(self) // 권장사항
}

클로저 기반 옵저버 관리

 
 
class ViewController: UIViewController {
    private var observers: [NSObjectProtocol] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 순환 참조 위험
        let badObserver = NotificationCenter.default.addObserver(
            forName: .dataUpdate,
            object: nil,
            queue: .main
        ) { notification in
            self.handleUpdate() // strong reference cycle!
        }
        
        // 안전한 방법
        let safeObserver = NotificationCenter.default.addObserver(
            forName: .dataUpdate,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            self?.handleUpdate() // weak reference
        }
        
        observers.append(safeObserver)
    }
    
    deinit {
        // 클로저 기반은 명시적 제거 필요
        observers.forEach { NotificationCenter.default.removeObserver($0) }
    }
}

메모리 관리 베스트 프랙티스

  1. 항상 [weak self] 사용: 클로저 기반 옵저버에서 순환 참조 방지
  2. 토큰 저장: 클로저 기반 옵저버의 반환 토큰을 저장하여 수동 해제 가능하게 함
  3. 명시적 해제: deinit에서 명시적으로 옵저버 제거
  4. 적절한 생명주기: 옵저버의 등록/해제 시점을 명확히 함

스레드 안전성

 
 
class ThreadSafeExample {
    func postFromBackground() {
        DispatchQueue.global().async {
            // 백그라운드에서 알림 발송 - 안전함
            NotificationCenter.default.post(name: .backgroundTask, object: nil)
        }
    }
    
    func observeWithSpecificQueue() {
        // 특정 큐에서 옵저버 실행
        NotificationCenter.default.addObserver(
            forName: .customEvent,
            object: nil,
            queue: .main // UI 업데이트용
        ) { notification in
            // 항상 메인 큐에서 실행됨
            print("현재 스레드: \(Thread.current)")
        }
        
        NotificationCenter.default.addObserver(
            forName: .heavyTask,
            object: nil,
            queue: OperationQueue() // 백그라운드 큐
        ) { notification in
            // 백그라운드에서 무거운 작업 처리
        }
    }
}

GQ3. NotificationCenter를 어떻게 관리해야 하는가?

주요 기능 + 적절한 활용 시점

1. 여러 객체가 한 이벤트에 반응할 때

 
 
// 로그인 이벤트 → 여러 화면이 동시에 업데이트
extension Notification.Name {
    static let userDidLogin = Notification.Name("userDidLogin")
}

class UserManager {
    func login(user: User) {
        authenticateUser(user)
        
        // 여러 화면에 로그인 완료 알림
        NotificationCenter.default.post(
            name: .userDidLogin,
            object: user,
            userInfo: ["loginTime": Date()]
        )
    }
}

2. 이벤트 소스를 모를 때

누가 이벤트를 발생시켰는지 확인할 필요 없이, 이벤트 발생 여부만 알면 됨

3. 시스템 이벤트 처리

 
 
// 키보드 이벤트 - 시스템에서 직접 발송
NotificationCenter.default.addObserver(
    forName: UIResponder.keyboardWillShowNotification,
    object: nil,
    queue: .main
) { [weak self] notification in
    if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
        self?.adjustUI(for: keyboardFrame.height)
    }
}

4. 깊은 뷰 계층에서의 전역 이벤트

 
 
// 5단계 깊이의 뷰 구조에서 최하단 → 최상단 통신
struct DeepNestedComponent: View {
    var body: some View {
        Button("전역 새로고침") {
            // @Binding으로 5단계 전달하기엔 너무 복잡
            NotificationCenter.default.post(name: .globalRefresh, object: nil)
        }
    }
}

언제 사용하지 말아야 하는가?

 NotificationCenter 사용하지 말아야 하는 경우

  1. 단순한 1:1 통신
 
// 대신 protocol/delegate 사용
protocol DataSource {
    func fetchData() -> [Item]
}

 

 

2.  SwiftUI에서 상태 관리

// 대신 @StateObject, @ObservableObject 사용
@StateObject var viewModel = ViewModel()

 

 

3. 직접적인 부모-자식 관계

struct ParentView: View {
    @State private var data = ""
    
    var body: some View {
        ChildView(data: $data) // 직접 전달
    }
}

 

 

 

실제 코드 예시

 

SwiftUI에서의 사용

struct ContentView: View {
    @State private var message = ""
    @State private var isKeyboardVisible = false
    
    var body: some View {
        VStack {
            Text(message)
            TextField("입력", text: .constant(""))
        }
        .padding(.bottom, isKeyboardVisible ? 250 : 0)
        .onReceive(NotificationCenter.default.publisher(for: .messageUpdate)) { notification in
            if let newMessage = notification.object as? String {
                message = newMessage
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
            isKeyboardVisible = true
        }
        .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
            isKeyboardVisible = false
        }
    }
}

커스텀 Notification Name 정의

extension Notification.Name {
    static let userDidLogin = Notification.Name("userDidLogin")
    static let dataDidUpdate = Notification.Name("dataDidUpdate")
    static let networkStatusChanged = Notification.Name("networkStatusChanged")
    static let messageUpdate = Notification.Name("messageUpdate")
    static let globalRefresh = Notification.Name("globalRefresh")
}

 

타입 안전한 NotificationCenter 래퍼

protocol TypedNotification {
    associatedtype Payload
    static var name: NSNotification.Name { get }
}

struct UserLoginNotification: TypedNotification {
    typealias Payload = User
    static let name = NSNotification.Name("user.login")
}

extension NotificationCenter {
    func post<T: TypedNotification>(_ type: T.Type, payload: T.Payload) {
        post(name: T.name, object: payload)
    }
    
    func observe<T: TypedNotification>(
        _ type: T.Type,
        queue: OperationQueue? = .main,
        handler: @escaping (T.Payload) -> Void
    ) -> NSObjectProtocol {
        return addObserver(forName: T.name, object: nil, queue: queue) { notification in
            guard let payload = notification.object as? T.Payload else { return }
            handler(payload)
        }
    }
}

// 타입 안전한 사용
NotificationCenter.default.post(UserLoginNotification.self, payload: user)
NotificationCenter.default.observe(UserLoginNotification.self) { user in
    print("사용자 로그인: \(user.name)")
}

 

성능 고려사항

최적화 팁

class OptimizedNotificationManager {
    // 효율적 방식 : 한 번 등록하고 조건부 처리
    func setupOptimizedObserver() {
        NotificationCenter.default.addObserver(
            forName: .frequentEvent,
            object: nil,
            queue: nil
        ) { [weak self] notification in
            guard let self = self,
                  self.shouldHandle(notification) else { return }
            self.handleNotification(notification)
        }
    }
    
    // 비효율적: 자주 등록/해제
    func inefficientPattern() {
        let observer = NotificationCenter.default.addObserver(/*...*/)
        // 짧은 시간 후
        NotificationCenter.default.removeObserver(observer)
    }
    
    // 효율적: 필요한 데이터만 전달
    func optimizedPosting() {
        NotificationCenter.default.post(
            name: .dataUpdate,
            object: nil,
            userInfo: ["id": dataId] // 최소한의 식별자만
        )
    }
}

디버깅 도구

 
// 디버깅용 NotificationCenter 모니터링
class NotificationDebugger {
    static func enableGlobalMonitoring() {
        NotificationCenter.default.addObserver(
            forName: nil, // 모든 알림 감지
            object: nil,
            queue: nil
        ) { notification in
            print("   알림: \(notification.name)")
            print("   발신자: \(notification.object ?? "nil")")
            print("   정보: \(notification.userInfo ?? [:])")
            print("   스레드: \(Thread.current)")
            print("---")
        }
    }
}

// 사용법
#if DEBUG
NotificationDebugger.enableGlobalMonitoring()
#endif

최신 기능 (iOS 15+)

AsyncSequence 활용

// 비동기 스트림으로 알림 처리
class AsyncNotificationHandler {
    func startListening() async {
        for await notification in NotificationCenter.default.notifications(
            named: .dataUpdate,
            object: nil
        ) {
            await handleDataUpdate(notification)
        }
    }
    
    private func handleDataUpdate(_ notification: Notification) async {
        // 비동기 처리
    }
}

 

 

728x90