iOS 개발자가 많이 하는 실수 - NotificationCenter addObserver 후 removeObserver 누락 문제
Dev Study/iOS 2025. 12. 4. 20:20반응형
iOS 개발자가 많이 하는 실수 - NotificationCenter addObserver 후 removeObserver 누락 문제
NotificationCenter는 iOS에서 매우 강력한 브로드캐스트 메커니즘이지만,
종종 observer를 제거하지 않아서 예상치 못한 동작, 중복 호출, 심지어 메모리 누수까지 경험하게 됩니다.
1. 문제 패턴: addObserver만 하고 removeObserver를 하지 않음
대표적인 코드:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification(_:)),
name: .someNotification,
object: nil
)
}
@objc private func handleNotification(_ notification: Notification) {
print("Received")
}
}
여기서 removeObserver를 호출하지 않으면 다음과 같은 문제가 발생할 수 있습니다.
- ViewController가 사라졌는데도 여전히 notification을 받으려고 시도
- 동일 객체가 여러 번 addObserver되면 중복으로 콜백이 여러 번 호출
- 내부적으로 참조가 남아서 메모리 릭 가능성 증가 (특히 커스텀 NotificationCenter 등)
2. 문제 증상
- 화면을 여러 번 열었다 닫으면 동일 이벤트가 여러 번 실행됨
- print("Received")가 1번이 아니라 2번, 3번씩 찍힘
- 이미 해제되었어야 할 객체가 여전히 콜백에 반응
- 의도하지 않은 타이밍에 콜백이 발생하여 상태가 꼬임
- 전체적인 흐름을 파악하기 매우 어려운 “유령 이벤트” 발생
3. 전통적인 add/remove 패턴
3-1. addObserver + removeObserver
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification(_:)),
name: .someNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc private func handleNotification(_ notification: Notification) {
print("Received")
}
}
- 최소한 deinit에서 removeObserver(self)를 호출해주면
객체가 해제될 때 자동으로 구독이 해제됩니다.
다만, iOS 9 이후 block 기반 API가 추가되면서
패턴 자체가 조금 변했습니다.
4. block 기반 addObserver 사용 시 주의사항
4-1. block 기반 API
let id = NotificationCenter.default.addObserver(
forName: .someNotification,
object: nil,
queue: .main
) { notification in
print("Received")
}
이 방식은 기존 selector 기반과 달리 토큰(id)을 반환합니다.
이 토큰은 나중에 remove할 때 필요합니다.
NotificationCenter.default.removeObserver(id)
4-2. self를 캡처할 때의 문제 (10번 항목과 연계)
class MyViewController: UIViewController {
private var token: Any?
override func viewDidLoad() {
super.viewDidLoad()
token = NotificationCenter.default.addObserver(
forName: .someNotification,
object: nil,
queue: .main
) { notification in
self.handle(notification) // ❌ self 강한 캡처
}
}
deinit {
if let token {
NotificationCenter.default.removeObserver(token)
}
}
private func handle(_ notification: Notification) {
print("Received")
}
}
- self → token을 strong으로 참조
- NotificationCenter → token을 strong으로 참조
- 클로저 → self를 strong으로 캡처
상황에 따라 retain cycle이 형성될 수 있으므로,
클로저에서는 [weak self] 패턴을 주로 사용합니다.
token = NotificationCenter.default.addObserver(
forName: .someNotification,
object: nil,
queue: .main
) { [weak self] notification in
self?.handle(notification)
}
5. iOS 9+에서의 일반적인 권장 패턴
Apple은 iOS 9 이후로 block 기반 API를 선호하며,
이 방식은 자동으로 observer 해제를 처리해주는 장점이 있습니다.
하지만 여전히 다음은 개발자가 설계해야 합니다.
- self 캡처 방식(weak/strong)
- token을 어디에 저장하고 언제 해제할 것인지
실무에서 많이 쓰는 패턴:
class MyViewController: UIViewController {
private var token: Any?
override func viewDidLoad() {
super.viewDidLoad()
token = NotificationCenter.default.addObserver(
forName: .someNotification,
object: nil,
queue: .main
) { [weak self] notification in
self?.handle(notification)
}
}
deinit {
if let token {
NotificationCenter.default.removeObserver(token)
}
}
private func handle(_ notification: Notification) {
print("Received:", notification)
}
}
6. selector 기반 addObserver에서 흔한 실수
6-1. 동일 객체에 중복 addObserver
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification),
name: .someNotification,
object: nil
)
}
만약 viewWillAppear에서 계속 addObserver만 하고
viewWillDisappear / deinit에서 removeObserver를 하지 않으면:
- 화면을 다시 들어올 때마다 observer가 하나씩 추가
- Notification 발생 시 콜백이 N번 호출 → 디버깅 난이도 매우 높음
→ 이 케이스는 실무에서 자주 터지는 패턴입니다.
해결책
- observer 등록 위치를 viewDidLoad 등 한 번만 호출되는 지점으로 옮기거나
- viewWillAppear에서 등록했다면 viewWillDisappear에서 반드시 해제
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
7. Notification을 쓸 때의 대안과 설계 관점
Notification은 전역 브로드캐스트이기 때문에:
- 흐름 추적이 어렵고
- 남용 시 전체 코드베이스를 복잡하게 만들 수 있습니다.
가능하다면:
- TCA, Combine, RxSwift, Delegation, Closure 콜백 등으로 대체
- Notification은 진짜 전역 브로드캐스트가 필요한 상황에서만 사용
예: 로그인 상태 변경, 테마 변경, 큰 앱 구조에서의 전역 이벤트 등
8. 실무용 체크리스트
- addObserver를 호출한 위치는 어디인가?
- viewDidLoad / viewWillAppear / init 등
- removeObserver는 언제 호출되는가?
- deinit / viewWillDisappear / 종료 시점 등
- observer가 중복으로 등록될 가능성은 없는가?
- 반복적으로 add만 하고 있지 않은지
- block 기반 API를 쓴다면 token을 어디에 저장하고 해제하는가?
- 클로저에서 self를 어떻게 캡처하는가? ([weak self] 고려)
9. 요약
- addObserver만 하고 removeObserver를 하지 않으면:
- 중복 이벤트
- 예측 불가 콜백
- 메모리 누수 가능성
이 발생한다.
- selector 기반:
- deinit 또는 적절한 라이프사이클 메서드에서 removeObserver(self) 호출
- block 기반:
- 반환된 token을 보관 → 해제 시 removeObserver(token) 호출
- 클로저에서는 [weak self] 사용
핵심 문장:
Notification을 구독했다면, 어디에서 해제할지까지 항상 함께 설계해야 한다.
반응형
'Dev Study > iOS' 카테고리의 다른 글
| iOS 개발자가 많이 하는 실수 - 스크롤/레이아웃 업데이트를 메인 스레드에서 실행하지 않아 UI가 깨지는 문제 (0) | 2025.12.04 |
|---|---|
| iOS 개발자가 많이 하는 실수 - JSONDecoder에서 Date 포맷/전략을 설정하지 않아 날짜 디코딩이 실패하는 실수 (0) | 2025.12.04 |
| iOS 개발자가 많이 하는 실수 - 네트워크 에러 및 HTTP 상태 코드(4xx/5xx)를 무시하는 실수 (0) | 2025.12.04 |
| iOS 개발자가 많이 하는 실수 - URLSession / 네트워크 요청후 UI 작업시 메인 스레드 전환을 잊는 실수 (0) | 2025.12.04 |
| iOS 개발자가 많이 하는 실수 - Timer invalidate() 누락으로 인한 메모리 누수·이상 동작 문제 (0) | 2025.12.04 |
| iOS 개발자가 많이 하는 실수 - 클로저에서 self를 강하게(strong) 캡처해 메모리 누수가 발생 (0) | 2025.12.04 |
| iOS 개발자가 많이 하는 실수 - Codable decode 실패 시 에러 원인 확인 없이 try? 사용 (0) | 2025.12.04 |
| iOS 개발자가 많이 하는 실수 - switch-case에서 default를 사용하고 enum 케이스 추가 시 실수 (0) | 2025.12.04 |

