| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- Task
- concurrency
- 스위프트
- async
- 프래그먼트
- View
- 프로그래머스
- SwiftUI
- 구조체
- weak
- 차이
- 프로퍼티
- 옵셔널
- 풀이
- RxSwift
- 생명주기
- 안드로이드
- ios
- 자바
- Self
- 백준
- 클로저
- rx
- 해시
- 연산자
- 리스트뷰
- 알고리즘
- Subject
- Swift
- observable
- Today
- Total
study record
[SwiftUI] .onAppear 말고 .task 본문
이 글을 쓰게 된 계기 :
Stop Using .onAppear() for Async Work in SwiftUI — Here’s Why It’s Causing Bugs in Your App
I learned this the hard way when my SwiftUI app randomly reloaded data every few seconds.
medium.com
0. SwiftUI의 .onAppear() — 당신이 생각하는 그게 아니다
UIKit에서 SwiftUI로 넘어온 많은 iOS 개발자들이 처음에 겪는 함정이 하나 있다.
바로 .onAppear()를 남용하는 것이다.
데이터를 불러오고, 이미지를 로드하고, CoreData를 동기화하고, 백그라운드 업데이트를 시작하는 등,
무언가 “화면이 보일 때” 해야 할 일을 전부 .onAppear()에 넣곤 한다.
처음엔 잘 작동한다.
그런데 어느 순간부터 API가 두 번씩 호출되고, 리스트가 깜빡이고,
뷰가 멈추지 않고 새로고침되는 이상한 현상이 발생한다.
게다가 더 짜증나는 건 이게 항상 재현되지 않는다는 점이다.
그 이유는 단순하다.
.onAppear()는 우리가 생각하는 viewDidLoad가 아니기 때문이다.
1. .onAppear()는 viewDidLoad가 아니다
UIKit에서는 뷰 생명주기가 명확했다.
viewDidLoad()는 한 번만 실행되고,
viewWillAppear()는 매번 화면이 나타날 때 호출된다.
하지만 SwiftUI의 뷰는 클래스가 아니라 구조체이다.
상태 변화나 상위 뷰의 리렌더링, SwiftUI의 최적화 과정에 따라 뷰가 여러 번 재생성될 수 있다.
즉, SwiftUI가 뷰를 다시 만들 때마다 .onAppear()가 또 호출된다.
API 호출, CoreData fetch, UI 상태 초기화가 반복되며
결과적으로 데이터 중복, 깜빡임, 비정상 동작이 일어난다.
2. .onAppear() 안의 async 작업은 위험하다
.onAppear {
Task {
await loadData()
}
}
겉보기엔 멀쩡하지만, 큰 함정이 있다.
이렇게 만든 Task는 뷰의 생명주기와 무관하게 계속 실행된다.
즉, 뷰가 사라져도 Task는 살아 있고,
완료되면 이미 사라진 @State를 업데이트하려 시도합니다.
그 결과가 바로 이런 크래시들입니다.
Publishing changes from background threads is not allowed
Fatal error: Modifying state after view is gone
3. SwiftUI의 정답: .task { }
iOS 15부터 Apple은 이런 문제를 해결하기 위해 .task { }를 도입했다.
언뜻 보면 .onAppear()와 비슷하지만, 본질적으로 다르다.
.task는 비동기 작업을 뷰의 생명주기에 맞춰 관리한다.
뷰가 사라지면 자동으로 Task를 cancel한다.
.task {
await loadUsers()
}
이렇게 하면:
- 뷰가 나타날 때 한 번만 실행되고
- 뷰가 사라지면 자동으로 취소됩니다
- 메모리 누수나 “좀비 Task” 문제도 없습니다
4. .task가 이렇게 잘 작동하는 이유
.task는 내부적으로 뷰의 identity와 연결되어 있다.
즉, SwiftUI가 뷰의 id나 상태가 바뀌어 다른 뷰로 인식하면
기존 Task를 취소하고 새로 시작한다.
이를 명시적으로 제어하고 싶다면 .task(id:)를 쓰면 됩니다.
.task(id: user.id) {
await fetchProfile(for: user)
}
이렇게 하면 user.id가 바뀔 때만 다시 실행되고,
그 외에는 중복 호출되지 않는다.
5. .onAppear()가 여전히 유용한 경우
.onAppear()가 나쁜 건 아니다.
단지 용도가 다를 뿐이다.
다음과 같은 동기 작업에는 여전히 적합하다.
.onAppear {
isVisible = true
analytics.log("UserList visible")
}
이처럼 UI 트리거, 애니메이션 시작, 로그 기록 등 await가 없는 가벼운 작업에만 쓰면 된다.
6. 리스트에서의 “유령 리로드” 문제
특히 List나 ForEach 안에서 .onAppear()를 쓰면 문제가 커진다.
ForEach(users) { user in
UserRow(user: user)
.onAppear {
Task {
await fetchProfilePicture(for: user)
}
}
}
스크롤할 때마다 셀이 재사용되며 .onAppear()가 계속 호출된다.
결국 백그라운드에서 네트워크 폭풍이 발생한다.
해결법은 .task(id:)를 사용하는 것이다.
ForEach(users) { user in
UserRow(user: user)
.task(id: user.id) {
await fetchProfilePicture(for: user)
}
}
7. 실전 사례
한 SwiftUI 기반 커머스 앱에서 “Home” 탭이 여러 API를 불러오는 구조였다.
.onAppear {
Task {
await fetchHomeData()
}
}
그런데 사용자가 탭을 전환할 때마다 API가 계속 호출되는 문제가 발생했다.
이유는 SwiftUI가 메모리 절약을 위해 탭 뷰를 재생성하기 때문이다.
즉, 탭을 다시 눌렀을 때 .onAppear()가 또 실행된 것이다.
.task { }로 바꾸자마자 문제는 완전히 사라졌다.
8. 요약
간단한 UI 업데이트, 애니메이션: onAppear { }
비동기 네트워크/데이터 작업: .task { }
특정 id 변경 시 재시작: .task(id: ) { }
함수 안에 await가 있다면, 그건 .onAppear()에 넣으면 안 됩니다.
9. iOS 14 이하 대응
.task가 없는 버전이라면, 수동으로 Task를 취소하세요.
.onAppear {
task = Task {
await loadData()
}
}
.onDisappear {
task?.cancel()
}
10. 정리 및 교훈
- SwiftUI 뷰는 짧게 살고 자주 바뀌는 구조체다.
- .onAppear()는 한 번만 실행된다는 보장이 없다.
- async 작업은 항상 취소 가능해야 한다 — .task가 그걸 해준다.
- “화면이 깜빡인다”, “API가 두 번 호출된다”, “데이터가 리셋된다”
- → 대부분은 .onAppear() 문제다.
'Swift' 카테고리의 다른 글
| [SwiftUI] 알아두면 유용한 사소한 SwiftUI 꿀팁 (0) | 2025.11.16 |
|---|---|
| [Swift] CoreData 알아보기 (0) | 2025.06.12 |
| [Swift] Noncopyable structs and enums (0) | 2025.03.03 |
| [Swift] consume, consuming, borrowing (0) | 2025.03.03 |