.task { } — async 수정자와 View에서의 데이터 로딩이란

저자: IT Sectr 게시일: 2026-06-26 읽는 시간: 9 분

.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 표시 시 비동기 데이터 로딩을 위한 SwiftUI 수정자, iOS 15부터 사용 가능.
  • 자동 취소 — View 사라질 때 SwiftUI가 Task를 취소하여 메모리 누수 방지.
  • async/await 컨텍스트 — .task 내에서 DispatchQueue나 Combine 없이 async 호출 사용 가능.
  • .task(id:) — 식별자가 있는 변형으로, 지정된 값이 변경되면 작업 재시작.
  • Structured Concurrency — .task는 병렬 작업을 위해 Structured Concurrency와 TaskGroup을 지원.

SwiftUI에서 .task { }란

.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가 자동으로 수행하므로 보일러플레이트 코드가 줄어들고 작업 취소를 잊어버릴 위험이 없어집니다.

swift
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
            }
        }
    }
}

.task vs .onAppear: 주요 차이점

많은 개발자가 .onAppear에서 데이터를 로드하는 데 익숙하지만, async/await와 .task의 등장으로 이 접근 방식은 구식이 되었습니다. .onAppear는 동기적으로 코드를 실행합니다 — .onAppear 내에서 비동기 작업을 수행하려면 호출을 Task { }로 래핑하고 취소 가능성을 위해 수동으로 참조를 유지해야 합니다. .task는 이를 자동으로 수행합니다.

특성.task { }.onAppear
Async 컨텍스트내장 async/awaitTask { } 래퍼 필요
자동 취소예, View 사라질 때아니요, 수동 구현 필요
Structured Concurrency지원미지원
재실행id 변경 시에만View 나타날 때마다
Apple 권장선호되는 방식동기 작업용

.onAppear는 여전히 동기 작업에 유용합니다 — 예를 들어, 로깅이나 초기 UI 설정. 그러나 비동기 데이터 로딩, 네트워크 요청, 데이터베이스 또는 파일 시스템 작업에는 .task를 사용하세요. 아키텍처 관점에서 더 안전하고 깔끔합니다.

swift
// ❌ 레거시 방식: .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:) — 데이터 변경 시 재시작

.task(id:) 수정자는 추가 매개변수인 식별자를 받습니다. 식별자 값이 변경되면 SwiftUI는 현재 작업을 취소하고 새 식별자로 새 작업을 시작합니다. 이는 데이터가 선택된 매개변수에 의존하는 화면에 이상적입니다 — 예를 들어, 카테고리별 문서 목록이나 ID별 제품 상세 정보.

swift
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를 수동으로 관리해야 합니다.

작업 취소 및 isCancelled 확인

.task는 View 사라질 때 작업을 자동으로 취소하지만, 비동기 작업 자체는 협력적으로 취소를 확인해야 합니다. Swift는 협력적 취소 모델을 사용합니다 — Task.cancel()은 실행을 강제로 중지하지 않고 isCancelled 플래그만 설정합니다. 작업 내부의 코드는 주기적으로 이 플래그를 확인해야 합니다.

swift
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 실용 예제

.task는 다양한 시나리오에 적합합니다: 간단한 JSON 로딩부터 TaskGroup을 사용한 복잡한 병렬 작업까지. 세 가지 일반적인 사용 사례를 살펴보겠습니다.

오류 처리와 함께 로딩

swift
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로 유지됨
            }
        }
    }
}

TaskGroup을 사용한 병렬 로딩

swift
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 일반적인 실수

가장 흔한 실수는 메인 스레드로 전환하지 않고 .task 내에서 UI 속성을 변경하는 것입니다. SwiftUI가 async 컨텍스트에서 @State를 수정할 때 자동으로 업데이트를 메인 스레드로 반환하지만, .task 내에서 UIKit 요소를 직접 조작하면 충돌이 발생할 수 있습니다.

  • try/catch 누락 — .task는 오류를 자동으로 처리하지 않습니다. 던지기 함수는 모두 do/catch로 래핑되어야 하며, 그렇지 않으면 앱이 충돌합니다.
  • 경합 조건 — 여러 .task(id:)가 다른 id로 시작되어 동일한 상태를 업데이트하면 경합 조건이 발생할 수 있습니다. 다른 데이터 소스에는 별도의 속성을 사용하세요.
  • 긴 동기 작업 — .task는 동기 코드를 비동기로 만들지 않습니다. .task 내에 무거운 동기 작업이 있으면 Task.detached로 래핑하거나 별도의 async 메서드로 이동하세요.
  • CancellationError 무시 — Task.checkCancellation()을 확인할 때 CancellationError는 위로 전파되어야 하며, 억제되지 않아야 합니다. 취소를 억제하면 메모리 누수가 발생할 수 있습니다.
swift
// ❌ 오류: 오류 처리 없음
.task {
    let data = await fetchData() // 오류 발생 시 충돌!
    items = data
}

// ✅ 올바름: do/catch
.task {
    do {
        items = await fetchData()
    } catch {
        errorMessage = error.localizedDescription
    }
}

자주 묻는 질문

.task와 .onAppear에서 Task { } 사용의 차이점은 무엇인가요?

.task는 View 사라질 때 작업을 자동 취소하고 Structured Concurrency를 지원합니다. .onAppear에서 Task { }는 작업에 대한 참조를 수동으로 유지하고 .onDisappear에서 cancel()을 호출해야 합니다. .task는 또한 읽기 쉽습니다 — 데이터 로딩이 View 생명주기의 일부임을 명시적으로 나타냅니다.

.task를 publisher 구독에 사용할 수 있나요?

네, AsyncSequence나 AsyncStream을 구독하려면 .task 내에서 for await value in publisher.values를 사용하세요. 이는 Publisher.values 확장을 통해 async/await와 Combine 모두에서 작동합니다. View가 사라지면 반복이 자동으로 종료되고 구독이 취소됩니다.

.task는 TabView와 어떻게 작동하나요 — 탭 전환 시 작업이 재시작되나요?

네, TabView 내의 View가 각 전환 시 재생성되는 경우 그렇습니다. iOS 18부터 TabView는 View를 메모리에 유지할 수 있습니다 — 이 경우 .task는 재시작되지 않습니다. 각 탭 전환 시 데이터를 다시 로드해야 하는 경우 탭 식별자와 함께 .task(id:)를 사용하세요.

.task가 있는 View가 요청 완료 전에 사라지면 어떻게 되나요?

SwiftUI는 View 사라질 때 작업을 취소합니다. URLSession 요청이 작업 내에 있었다면 함께 취소됩니다. 작업이 협력적 취소를 지원하지 않는 경우(isCancelled를 확인하지 않는 등), 계속 실행되지만 View가 더 이상 존재하지 않으므로 결과가 상태에 적용되지 않습니다.

하나의 View에 여러 .task 수정자를 사용할 수 있나요?

네, 하나의 View에 여러 .task 수정자를 추가할 수 있습니다. 각각은 독립적인 작업을 생성합니다. 이는 다른 데이터 소스를 분리하는 데 유용합니다: 프로필 로딩용 .task, WebSocket 구독용 .task, 위치 모니터링용 .task 등.

요약

  • .task { } — View 사라질 때 자동 취소와 함께 비동기 작업을 위한 SwiftUI 수정자.
  • async/await 지원 — .task 내에서 Task 래퍼 없이 완전한 async 컨텍스트 사용 가능.
  • .task(id:) — 식별자 변경 시 작업 재시작, 수동 .onChange 대체.
  • 협력적 취소 — 작업 내 취소 확인을 위해 Task.checkCancellation() 사용.
  • Structured Concurrency — .task는 하위 작업 취소와 함께 TaskGroup 및 병렬 작업 지원.
  • .onAppear 대체 — 비동기 데이터 로딩에는 .onAppear + Task + .onDisappear 패턴 대신 .task 사용.
  • 오류 처리 — 충돌 방지를 위해 .task 내 모든 async 호출은 do/catch로 래핑되어야 함.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기