iOS 개발자가 많이 하는 실수 - weak self를 빼먹어 강한 순환 참조(Strong Reference Cycle)로 메모리 누수가 발생하는 실수
Dev Study/iOS 2025. 12. 4. 20:52반응형
iOS 개발자가 많이 하는 실수 - weak self를 빼먹어 강한 순환 참조(Strong Reference Cycle)로 메모리 누수가 발생하는 실수
iOS 개발에서 가장 흔한 메모리 누수 원인은 강한 순환 참조(Strong Reference Cycle)입니다.
특히 클로저(closure) 내부에서 self를 사용할 때 weak self 또는 unowned self를 쓰지 않으면
뷰 컨트롤러, 뷰모델, 매니저 객체 등이 해제되지 않고 메모리에 계속 남아있는 문제가 발생합니다.
1. 강한 순환 참조란 무엇인가?
Swift에서는 기본적으로 클래스 타입이 참조(reference)로 관리됩니다.
예:
class ViewModel {
var onUpdate: (() -> Void)?
}
여기에서 다음 구조가 자주 나타납니다:
- ViewController가 ViewModel을 강한 참조
- ViewModel의 클로저가 ViewController(self)를 강한 참조
즉:
ViewController → ViewModel → Closure → ViewController
이렇게 서로를 강하게 잡고 있으면:
- 화면에서 사라져도 ViewController가 해제되지 않음
- 메모리 누수 발생
- 다시 화면에 진입하면 객체가 중복 생성되어 버그 발생 가능
2. 문제 패턴: 클로저에서 self를 직접 캡처
예:
class MyViewController: UIViewController {
let viewModel = MyViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.onUpdated = {
self.updateUI() // ❌ 강한 self 캡처
}
}
}
이 경우:
- viewModel이 해제되기 전까지 VC도 같이 유지됨
- 화면을 dismiss/popup/back 해도 VC 인스턴스가 메모리에 남음
- 다시 push하면 여러개의 VC가 중첩되어 메모리/로직 꼬임
3. 해결 방법: weak self / unowned self 사용
3-1. 가장 흔한 패턴: weak self
viewModel.onUpdated = { [weak self] in
guard let self = self else { return }
self.updateUI() // ✔️ 안전한 self 캡처
}
특징:
- self가 해제되면 클로저는 nil이 되므로 안전
- 대부분 상황에서 가장 추천되는 방식
3-2. unowned self (주의)
viewModel.onUpdated = { [unowned self] in
self.updateUI()
}
특징:
- self를 nil 체크 없이 바로 사용
- self가 해제된 상태에서 호출되면 크래시 발생
- 반드시 self의 라이프사이클이 클로저보다 더 길다는 것이 확실할 때만 사용
예: 애니메이션 블록, 특정 API 내부에서 보장되는 짧은 생명 주기
4. async/await에서도 동일한 문제 발생
많은 개발자가 간과하는 부분:
Task {
await viewModel.load()
self.updateUI() // ❌ 강한 self 캡처
}
Swift Concurrency도 클로저 기반이기 때문에 self가 강하게 캡처됨.
해결:
Task { [weak self] in
guard let self else { return }
await viewModel.load()
self.updateUI()
}
또는:
Task { @MainActor [weak self] in
guard let self else { return }
await viewModel.load()
self.updateUI()
}
5. UIView.animate, Timer, NotificationCenter에서도 강한 self 발생
5-1. UIView.animate
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0 // ❌ self 캡처
}
해결:
UIView.animate(withDuration: 0.3) { [weak self] in
self?.view.alpha = 0
}
5-2. Timer
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.updateTime() // ❌ self 캡처
}
해결:
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.updateTime()
}
5-3. NotificationCenter
NotificationCenter.default.addObserver(
forName: .keyboardWillShow,
object: nil,
queue: .main
) { notification in
self.handleKeyboard(notification) // ❌ self 캡처
}
해결:
NotificationCenter.default.addObserver(
forName: .keyboardWillShow,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleKeyboard(notification)
}
6. RxSwift, Combine, TCA에서도 동일한 패턴
Combine
publisher
.sink { value in
self.value = value // ❌
}
해결:
publisher
.sink { [weak self] value in
self?.value = value
}
RxSwift
observable.subscribe(onNext: { value in
self.value = value // ❌
})
해결:
observable.subscribe(onNext: { [weak self] value in
self?.value = value
})
TCA
Reducer 내부의 Effect나 Combine Publisher에서도 동일하게 weak self 필요.
7. 강한 순환 참조를 확실하게 감지하는 방법
7-1. deinit 확인하기
ViewController에 다음을 추가:
deinit {
print("MyViewController deinit")
}
화면을 닫았는데도 deinit이 출력되지 않으면 메모리 누수입니다.
7-2. Xcode Instruments → Leaks / Allocations 사용
- Leaks Instrument에서 객체가 해제되지 않는지 확인
- Allocations에서 같은 화면을 여러 번 push했을 때 VC 인스턴스가 증가하는지 확인
8. 실무용 체크리스트
다음 클로저들 안에서 self를 쓰는 경우 반드시 점검해야 함:
- 네트워크 completion handler
- async/await Task block
- UIView.animate
- URLSession.dataTask
- DispatchQueue.async
- Timer handler
- NotificationCenter observer
- Combine sink
- RxSwift subscribe
- TCA EffectPublisher sink
질문:
- 이 클로저가 self보다 오래 살아남을 가능성이 있는가?
- self가 필요 없다면 캡처하지 않아도 되는가?
- self가 필요하다면 weak/unowned 중 무엇이 안전한가?
- VC의 deinit이 정상적으로 호출되는가?
9. 요약
- 클로저는 기본적으로 self를 강하게 캡처한다.
- self가 해제되어도 클로저가 self를 붙들고 있으면
강한 순환 참조로 인해 메모리 누수가 발생한다. - 해결:
- [weak self] 사용 + guard let self 패턴
- 특정한 경우에만 [unowned self]
- 비동기 작업, Timer, NotificationCenter, Combine/Rx, TCA 등
모든 비동기 시나리오에서 동일하게 적용됨.
핵심 문장:
클로저 = self 캡처. self가 필요하면 weak self로 안전하게 캡처하라.
반응형

