반응형
iOS 개발자가 많이 하는 실수 - Timer invalidate() 누락으로 인한 메모리 누수·이상 동작 문제
Timer는 iOS에서 매우 자주 사용하는 도구이지만,
초보자는 물론 경력자도 가끔씩 invalidate를 빼먹어서
예상치 못한 메모리 누수나 이상 동작을 겪곤 합니다.
1. 문제 패턴: 만든 Timer를 끝까지 정리하지 않음
대표적인 코드:
class MyViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("Tick")
}
}
}
여기서 invalidate를 호출하지 않으면:
- ViewController가 화면에서 사라져도 Timer는 계속 돌 수 있음
- Timer가 내부적으로 self를 캡처하고 있다면
ViewController 메모리가 해제되지 않는 상황이 발생
2. Timer와 RunLoop, 그리고 Retain Cycle
Timer.scheduledTimer는 기본적으로 현재 RunLoop에 등록되며,
RunLoop는 Timer를 강하게 참조합니다.
추가로, Timer의 handler 클로저에서 self를 강하게 캡처하면:
- self → timer를 strong으로 참조 (프로퍼티)
- timer → 클로저를 strong으로 참조
- 클로저 → self를 strong으로 캡처
이렇게 순환 참조(Retain Cycle)가 만들어집니다.
결과:
- ViewController의 deinit이 호출되지 않음
- 화면에서 사라졌는데도 Timer가 계속 실행
- 예상치 못한 로그, 네트워크 요청, 애니메이션 등이 계속 발생
3. invalidate를 누락했을 때 나타나는 증상
- ViewController가 사라져도 print("Tick")이 계속 찍힘
- API 요청/타이머 기반 갱신이 멈추지 않음
- ViewController 인스턴스가 계속 메모리에 남아 있음
- deinit이 호출되지 않음
- 시간이 지나면서 메모리 사용량 증가, 성능 저하
4. 올바른 Timer 사용 패턴
4-1. 최소한 deinit에서 invalidate
class MyViewController: UIViewController {
private var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
}
deinit {
timer?.invalidate()
}
private func startTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self else { return }
self.tick()
}
}
private func tick() {
print("Tick")
}
}
포인트:
- 새로운 Timer를 만들기 전에 이전 Timer를 invalidate
- deinit에서도 마지막 방어선으로 invalidate
- 클로저에서는 [weak self] 사용 (10번 항목과 연계)
4-2. 특정 시점에서 명시적으로 중단하기
화면이 사라질 때, 또는 기능이 종료될 때 Timer를 끄고 싶을 때:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
timer = nil
}
상황에 따라:
- viewDidDisappear
- 버튼 클릭 후 종료
- 특정 조건 충족 시 종료
등 앱 로직에 맞춰 명시적으로 Timer를 멈추는 지점을 갖는 것이 중요합니다.
4-3. repeats: false인 Timer라도 명심할 점
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
self.doSomething()
}
- 단발성 Timer는 실행 후 자동으로 invalid 상태가 됩니다.
- 다만, 클로저에서 self를 강하게 캡처하면 Timer가 살아 있는 동안 retain cycle이 잠깐 생길 수는 있습니다.
- 대부분의 경우 큰 문제는 아니지만,
컨텍스트에 따라 [weak self]를 유지하는 편이 일관되고 안전합니다.
5. CADisplayLink, DispatchSourceTimer 등 유사 패턴
Timer뿐 아니라 다음 객체들도 비슷한 문제를 일으킬 수 있습니다.
- CADisplayLink
- DispatchSourceTimer
- 커스텀 타이머/스케줄러 객체
공통 점:
- 반복적으로 호출된다
- 콜백에서 self를 강하게 캡처하기 쉽다
- 종료/해제 시점을 명확히 정의하지 않으면 메모리/동작 문제 발생
따라서 Timer에 대해 배운 패턴은
이런 다른 반복 콜백 객체에도 동일하게 적용해야 합니다.
6. 실무용 체크리스트
Timer를 도입할 때 다음 질문들을 항상 점검하면 좋습니다.
- Timer를 어디에서 생성하는가?
- viewDidLoad? viewDidAppear? 별도의 Manager?
- Timer를 언제 멈출 것인가?
- viewWillDisappear / viewDidDisappear / deinit / 특정 로직 완료 시점
- Timer가 self를 어떻게 캡처하는가?
- 클로저에서 [weak self]를 사용했는가?
- 새 Timer를 만들기 전에 이전 Timer를 invalidate 했는가?
- 중복 생성으로 인해 호출 횟수가 2배, 3배로 늘어나지 않는가?
7. 요약
- Timer는 매우 자주 쓰이지만, invalidate를 빼먹으면 거의 항상 문제가 된다.
- 문제 증상:
- ViewController deinit이 호출되지 않음
- 화면이 사라져도 Timer 콜백이 계속 돈다
- 메모리·동작 이상
- 안전한 사용을 위해:
- Timer를 프로퍼티로 관리
- 새로 만들기 전에 이전 Timer를 invalidate
- deinit 또는 적절한 라이프사이클 메서드에서 invalidate
- 클로저에서는 [weak self] 사용
핵심 문장:
반복 Timer를 만들었다면, 어디서 끝낼지를 반드시 함께 설계해야 한다.
반응형
'Dev Study > iOS' 카테고리의 다른 글
| iOS 개발자가 많이 하는 실수 - JSONDecoder에서 Date 포맷/전략을 설정하지 않아 날짜 디코딩이 실패하는 실수 (0) | 2025.12.04 |
|---|---|
| iOS 개발자가 많이 하는 실수 - 네트워크 에러 및 HTTP 상태 코드(4xx/5xx)를 무시하는 실수 (0) | 2025.12.04 |
| iOS 개발자가 많이 하는 실수 - URLSession / 네트워크 요청후 UI 작업시 메인 스레드 전환을 잊는 실수 (0) | 2025.12.04 |
| iOS 개발자가 많이 하는 실수 - NotificationCenter addObserver 후 removeObserver 누락 문제 (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 |
| iOS 개발자가 많이 하는 실수 - Dictionary에서 key 존재 여부 확인 없이 강제 언래핑(!) (0) | 2025.12.04 |

