.task { }는 iOS 15에서 도입된 SwiftUI 수정자로, View가 나타날 때 비동기 작업을 시작하고 View가 사라질 때 자동으로 취소합니다. 취소 기능 없이 동기 코드를 실행하는 .onAppear와 달리, .task는 async/await 컨텍스트에서 작동하며 View의 생명주기를 고려합니다. View가 사라지면 SwiftUI는 생성된 Task에서 cancel()을 호출합니다. 이는 메모리 누수와 View 업데이트가 더 이상 필요하지 않은 후에도 작업이 실행되는 것을 방지합니다. Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024)에 따르면, .task는 Structured Concurrency와 안전하게 작동하고 비동기 작업의 수명을 자동으로 관리하기 때문에 SwiftUI에서 데이터를 로드하는 선호되는 방법입니다.
핵심 포인트
.task { }는 View가 화면에 나타날 때 async 컨텍스트에서 Task를 생성하는 View 수정자입니다. SwiftUI는 제공된 클로저를 백그라운드 스레드에서 실행하여 메인 스레드를 UI 작업을 위해 자유롭게 둡니다. View가 사라지면 SwiftUI는 Structured Concurrency 메커니즘을 통해 Task를 자동으로 취소합니다. 이는 비동기 작업의 결과가 더 이상 필요하지 않은 후에도 실행이 계속되지 않도록 보장합니다.
Apple — Swift Programming Language (2025)에 따르면, .task는 Swift 5.5에서 도입된 Structured Concurrency 개념을 사용합니다. 각 .task는 부모 View의 작업 아래에 하위 작업을 생성합니다. 부모 작업이 취소되면(View가 사라지면), 모든 하위 작업도 자동으로 취소됩니다. 이는 DispatchWorkItem이나 AnyCancellable에 대한 참조를 수동으로 저장하는 것과 비교하여 비동기 작업의 생명주기 관리를 근본적으로 단순화합니다.
@State + .onAppear에서의 수동 호출이라는 전통적인 접근 방식과 달리, .task는 이후 취소를 위해 Task에 대한 참조를 저장할 필요가 없습니다. SwiftUI가 자동으로 수행하므로 보일러플레이트 코드가 줄어들고 작업 취소를 잊어버릴 위험이 없어집니다.
struct ArticlesView: View {
@State var articles: [Article] = []
@State var error: Error?
var body: some View {
List(articles) { article in
Text(article.title)
}
.task {
do {
articles = await APIClient().fetchArticles()
} catch {
self.error = error
}
}
}
}
많은 개발자가 .onAppear에서 데이터를 로드하는 데 익숙하지만, async/await와 .task의 등장으로 이 접근 방식은 구식이 되었습니다. .onAppear는 동기적으로 코드를 실행합니다 — .onAppear 내에서 비동기 작업을 수행하려면 호출을 Task { }로 래핑하고 취소 가능성을 위해 수동으로 참조를 유지해야 합니다. .task는 이를 자동으로 수행합니다.
| 특성 | .task { } | .onAppear |
|---|---|---|
| Async 컨텍스트 | 내장 async/await | Task { } 래퍼 필요 |
| 자동 취소 | 예, View 사라질 때 | 아니요, 수동 구현 필요 |
| Structured Concurrency | 지원 | 미지원 |
| 재실행 | id 변경 시에만 | View 나타날 때마다 |
| Apple 권장 | 선호되는 방식 | 동기 작업용 |
.onAppear는 여전히 동기 작업에 유용합니다 — 예를 들어, 로깅이나 초기 UI 설정. 그러나 비동기 데이터 로딩, 네트워크 요청, 데이터베이스 또는 파일 시스템 작업에는 .task를 사용하세요. 아키텍처 관점에서 더 안전하고 깔끔합니다.
// ❌ 레거시 방식: .onAppear에서 취소 없는 Task
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
.onAppear {
loadTask = Task { await loadData() }
}
.onDisappear { loadTask?.cancel() }
// ✅ 현대적 방식: .task가 취소 관리
func body() { var body: some View { Text("") }
.task { await loadData() }
.task(id:) 수정자는 추가 매개변수인 식별자를 받습니다. 식별자 값이 변경되면 SwiftUI는 현재 작업을 취소하고 새 식별자로 새 작업을 시작합니다. 이는 데이터가 선택된 매개변수에 의존하는 화면에 이상적입니다 — 예를 들어, 카테고리별 문서 목록이나 ID별 제품 상세 정보.
struct CategoryView: View {
let categoryId: Int
@State var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task(id: categoryId) {
await loadItems(for: categoryId)
}
}
func loadItems(for id: Int) async {
do {
items = await APIClient().fetchItems(categoryId: id)
} catch {
// 오류 처리
}
}
}
categoryId가 변경되면 SwiftUI는 이전 요청을 취소하고 새 요청을 시작합니다. 이는 빠른 카테고리 전환 시 특히 중요합니다 — 오래된 요청이 새 요청과 상태 업데이트를 놓고 경쟁하지 않습니다. .task(id:)가 없으면 .onChange를 통해 수동으로 변경 사항을 추적하고 Task를 수동으로 관리해야 합니다.
.task는 View 사라질 때 작업을 자동으로 취소하지만, 비동기 작업 자체는 협력적으로 취소를 확인해야 합니다. Swift는 협력적 취소 모델을 사용합니다 — Task.cancel()은 실행을 강제로 중지하지 않고 isCancelled 플래그만 설정합니다. 작업 내부의 코드는 주기적으로 이 플래그를 확인해야 합니다.
struct LoadingView: View {
@State var progress: Double = 0
var body: some View {
ProgressView(value: progress)
.task {
for i in 0..<100 {
// 취소 확인
try Task.checkCancellation()
await Task.sleep(nanoseconds: 50_000_000)
progress = Double(i + 1) / 100.0
}
}
}
}
Task.checkCancellation()은 작업이 취소된 경우 CancellationError를 던집니다. 이것이 가장 간단한 확인 방법입니다 — 모든 async 컨텍스트에서 작동합니다. 대안으로 비용이 많이 드는 작업 전에 Task.isCancelled를 수동으로 확인할 수 있습니다. URLSession의 경우, 작업이 취소되면 네트워크 요청이 자동으로 취소됩니다. URLSession이 Structured Concurrency를 기본 지원하기 때문입니다.
.task는 다양한 시나리오에 적합합니다: 간단한 JSON 로딩부터 TaskGroup을 사용한 복잡한 병렬 작업까지. 세 가지 일반적인 사용 사례를 살펴보겠습니다.
struct ProfileView: View {
@State var profile: Profile?
@State var isLoading = true
var body: some View {
Group {
if isLoading {
ProgressView()
} else if let profile {
Text(profile.name)
} else {
Text("로드 실패")
}
}
.task {
defer { isLoading = false }
do {
profile = await APIClient().fetchProfile()
} catch {
// 프로필이 nil로 유지됨
}
}
}
}
struct DashboardView: View {
@State var stats: DashboardStats?
var body: some View {
Text("대시보드")
.task {
stats = await Task {
await withThrowingTaskGroup { group in
group.addTask { await API().fetchUsers() }
group.addTask { await API().fetchOrders() }
group.addTask { await API().fetchRevenue() }
return DashboardStats(
users: try await group.next(),
orders: try await group.next(),
revenue: try await group.next()
)
}
}.value
}
}
}
가장 흔한 실수는 메인 스레드로 전환하지 않고 .task 내에서 UI 속성을 변경하는 것입니다. SwiftUI가 async 컨텍스트에서 @State를 수정할 때 자동으로 업데이트를 메인 스레드로 반환하지만, .task 내에서 UIKit 요소를 직접 조작하면 충돌이 발생할 수 있습니다.
// ❌ 오류: 오류 처리 없음
.task {
let data = await fetchData() // 오류 발생 시 충돌!
items = data
}
// ✅ 올바름: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
자주 묻는 질문
.task는 View 사라질 때 작업을 자동 취소하고 Structured Concurrency를 지원합니다. .onAppear에서 Task { }는 작업에 대한 참조를 수동으로 유지하고 .onDisappear에서 cancel()을 호출해야 합니다. .task는 또한 읽기 쉽습니다 — 데이터 로딩이 View 생명주기의 일부임을 명시적으로 나타냅니다.
네, AsyncSequence나 AsyncStream을 구독하려면 .task 내에서 for await value in publisher.values를 사용하세요. 이는 Publisher.values 확장을 통해 async/await와 Combine 모두에서 작동합니다. View가 사라지면 반복이 자동으로 종료되고 구독이 취소됩니다.
네, TabView 내의 View가 각 전환 시 재생성되는 경우 그렇습니다. iOS 18부터 TabView는 View를 메모리에 유지할 수 있습니다 — 이 경우 .task는 재시작되지 않습니다. 각 탭 전환 시 데이터를 다시 로드해야 하는 경우 탭 식별자와 함께 .task(id:)를 사용하세요.
SwiftUI는 View 사라질 때 작업을 취소합니다. URLSession 요청이 작업 내에 있었다면 함께 취소됩니다. 작업이 협력적 취소를 지원하지 않는 경우(isCancelled를 확인하지 않는 등), 계속 실행되지만 View가 더 이상 존재하지 않으므로 결과가 상태에 적용되지 않습니다.
네, 하나의 View에 여러 .task 수정자를 추가할 수 있습니다. 각각은 독립적인 작업을 생성합니다. 이는 다른 데이터 소스를 분리하는 데 유용합니다: 프로필 로딩용 .task, WebSocket 구독용 .task, 위치 모니터링용 .task 등.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.