본문 바로가기

Programming/Swift

Swift Concurrency: Task {} vs Task.detached {}

반응형

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을 통해 접근하세요

 

반응형