저번에 async/await 관한 글을 쓸 때 Task에 대해 간략히 말했었습니다. 그래서 이번 글에서는 Task에 대해 알아볼게요
Task는 비동기 작업 하나를 나타내는 타입입니다. 동기 컨텍스트 어디서든 만들 수 있습니다.
Button("불러오기") {
// 여기는 동기 컨텍스트
Task {
// 여기는 비동기 컨텍스트
await viewModel.load()
}
}
반환값에 접근하고 싶다면 await으로 기다릴 수 있습니다.
let task = Task {
return try await fetchUser(id: 42)
}
let user = try await task.value
Swift Concurrency에는 Structured concurrency, Unstructured concurrency 이렇게 두 개의 큰 모델이 있습니다. Task는 이 중에서 Unstructured 모델입니다.
Structured(구조적) concurrency에 대해 간단히 알아보면 코드의 흐름(scope)에 종속되는 방식입니다.
func loadAll() async throws {
async let user = fetchUser()
async let posts = fetchPosts()
// 둘 다 끝날 때까지 여기서 대기
let (u, p) = try await (user, posts)
}
Unstructured(비구조적) concurrency는 scope와 독립적으로 살아있는 방식입니다.
func onButtonTap() {
Task { await doSomething() } // scope를 벗어나도 계속 실행
}
위에서 Task는 Unstructured concurrency에 속하며, 취소가 자동 전파가 안 되기 때문에 취소 하려면 직접 cancel()를 호출해야 합니다.
let task = Task { await doHeavyWork() }
task.cancel() // 취소 요청
cancel()은 취소를 요청할 뿐 즉시 멈추지 않으며, Task 내부에서 직접 확인해야 실제로 멈춥니다.
취소 여부 확인은 다음과 같은 방식으로 확인 가능합니다.
Task {
for item in largeList {
guard !Task.isCancelled else { return }
process(item)
}
}
Task는 우선순위를 지정할 수 있습니다.
Task(priority: .userInitiated) { await loadCriticalData() }
Task(priority: .background) { await prefetchImages() }
| 우선순위 | 용도 |
|---|---|
| .userInteractive | 즉각적인 UI 반응 |
| .userInitiated | 사용자가 시작한 작업 |
| .medium | 기본값 |
| .utility | 진행 표시줄이 있는 작업 |
| .background | 사용자가 인식하지 못하는 작업 |
지정하지 않으면 현재 컨텍스트의 우선순위를 상속합니다.
Task(priority: .background) {
Task { // .background를 상속
await work()
}
}
아까 Task는 현재 컨텍스트의 actor와 priority를 상속한다고 말했죠
@MainActor
class MyViewModel: ObservableObject {
func load() {
Task {
// @MainActor 상속 → Main Thread에서 실행
self.state = .loading
let data = try await fetch()
self.state = .loaded(data)
}
}
}
@MainActor 안에서 만든 Task는 Main Thread에서 실행되므로 self.state 접근이 안전합니다.
하지만 Task.detached는 아닙니다. Task.detached는 @MainActor를 상속하지 않기 때문에, self.state에 접근하면 Main Thread가 아닌 곳에서 UI 상태를 건드리게 될 수도 있기 때문에 사용할 때 다음 방식을 권장합니다.
Task.detached(priority: .background) {
let result = await heavyWork() // 백그라운드에서 처리
await MainActor.run {
self.state = result // Main Thread에서 안전하게 접근
}
}
Task.detached(priority: .background) {
let result = await heavyWork()
await self.applyResult(result)
}
@MainActor
private func applyResult(_ result: String) {
self.state = result
}
대부분은 Task { }로 충분합니다. detached는 Main Actor의 영향을 피해야 하는 무거운 작업(이미지 처리, 파일 압축 등)에만 사용하세요.
| Task { } | Task.detached { } | |
|---|---|---|
| Actor 상속 | O | X |
| Priority 상속 | O | X |
| @MainActor 클래스 안에서 self 접근 | 안전 | 위험 |
| 해결책 | 없어도 됨 | MainActor.run 또는 @MainActor 함수 분리 |
| 주요 용도 | 일반적인 비동기 작업 | Main Actor 밖에서 실행할 무거운 작업 |