반응형
Swift Concurrency를 사용할 때 자주 헷갈리는 부분 중 하나가 바로 Task {}와 Task.detached {}의 차이입니다.
특히, UI 업데이트나 백그라운드 작업을 처리할 때 어떤 방식을 써야 하는지에 따라 안전성과 취소 동작에서 큰 차이가 발생합니다.
이번 글에서는 Stackademic 블로그의 "Task vs Task.detached in Swift: The Concurrency Trap" 글을 바탕으로 내용을 정리하고, 추가 예제를 곁들여 설명합니다.
핵심 요약
- Task {}
- 구조적(structured), 현재 context 상속 (actor, priority, task-local)
- 부모 작업이 취소되면 함께 취소됨
- UI/actor-isolated 컨텍스트에 안전
- Task.detached {}
- 비구조적(unstructured), context 상속 없음
- 부모 취소와 무관하게 독립 실행
- actor-isolated 리소스 접근 시 race condition 위험
차이점 비교표
항목 | Task {} | Task.detached {} |
Context 상속 여부 | ✅ 상속 (actor, priority, task-locals) | ❌ 상속하지 않음 |
취소 동작 | 부모 취소 시 함께 취소됨 | 독립 실행 (수동 취소 필요) |
사용 시점 | UI 업데이트, actor 내부 로직 | 독립적인 백그라운드 작업 |
안전성 | actor 격리에 안전 | race condition 위험 |
예제 1: 취소 동작 차이
func exampleCancellation() {
let parent = Task {
print("Parent started")
try? await Task.sleep(nanoseconds: 2_000_000_000)
print("Parent finished")
}
Task {
try? await Task.sleep(nanoseconds: 500_000_000)
parent.cancel()
print("Parent cancelled")
}
}
- Task {} 내부에서 만든 작업은 부모가 취소되면 자동으로 취소됨.
- 하지만 Task.detached라면 부모 취소와 관계없이 끝까지 실행됩니다.
예제 2: Actor 격리(Actor Isolation) 문제
@MainActor
class ViewModel {
var title = "Hello"
func updateWrong() {
Task.detached {
// ❌ UI thread를 상속하지 않아서 crash나 race 발생 가능
self.title = "World"
}
}
func updateSafe() {
Task.detached {
await MainActor.run {
// ✅ 안전하게 UI 접근
self.title = "World"
}
}
}
}
예제 3: Priority 상속 차이
Task(priority: .high) {
print("Parent task priority: \(Task.currentPriority)")
Task {
print("Child task priority: \(Task.currentPriority)")
// -> .high 상속됨
}
Task.detached {
print("Detached task priority: \(Task.currentPriority)")
// -> 기본 priority (보통 .medium)
}
}
- Task {}는 부모의 priority를 그대로 상속받습니다.
- Task.detached {}는 독립적이므로 별도의 priority를 가집니다.
예제 4: Task Local 값 상속 여부
Swift Concurrency는 TaskLocal을 통해 task-local storage를 제공합니다.
enum LoggerKey: TaskLocalKey {
static let defaultValue: String = "default"
}
extension TaskLocal where Key == LoggerKey {
static var logger: String {
get { LoggerKey.defaultValue }
set { self[LoggerKey.self] = newValue }
}
}
Task {
await TaskLocal.$logger.withValue("UIFlow") {
print("Parent logger:", TaskLocal.logger)
Task {
print("Child logger:", TaskLocal.logger) // "UIFlow" 상속됨
}
Task.detached {
print("Detached logger:", TaskLocal.logger) // "default" (상속 안됨)
}
}
}
언제 무엇을 써야 할까?
- Task {}
- UI 업데이트
- ViewModel 내부에서 네트워크 요청 후 상태 변경
- 부모-자식 관계 및 취소 제어가 필요한 경우
- Task.detached {}
- 완전히 독립적인 백그라운드 작업
- 로그 저장, 캐시 청소, 백업 등 UI/Actor 격리와 무관한 작업
- 단, 꼭 필요할 때만 사용! 남발 시 관리 어려움
결론
- 대부분의 경우 Task {}를 기본값으로 사용하세요.
- 정말 독립적인 실행이 필요할 때만 Task.detached {}를 선택하세요.
- 특히 UI 작업에서는 절대 Task.detached로 직접 상태를 건드리지 말고, 반드시 MainActor.run을 통해 접근하세요
반응형
'Programming > Swift' 카테고리의 다른 글
Swift struct 배열에서 내부 값 수정하기 (0) | 2025.04.08 |
---|---|
Swift OptionSet으로 깔끔한 옵션 관리하기 (0) | 2025.03.24 |
Actor가 처음이라고요? (0) | 2025.01.01 |
swift 주석 관련 공식 문서 (Swift Markup Formatting Reference) (0) | 2024.08.01 |
MVVM+Router (0) | 2024.04.18 |