Swift Concurrency: Task Dive

Ios_Roy·2025년 8월 27일
0

TIL

목록 보기
22/25
post-thumbnail

Swift Concurrency: Task 완벽 정리

Swift 5.5부터 도입된 Concurrency(비동기/동시성)는 기존의 GCD나 Operation보다 훨씬 안전하고 직관적인 코드를 작성할 수 있게 해줍니다.
그 중심에는 바로 Task라는 개념이 있습니다. 이번 글에서는 Task를 중심으로 Swift Concurrency의 핵심과 함께, SwiftUI와의 결합 패턴, 네트워크/데이터 처리 Best Practice, 그리고 Actor / MainActor / TaskLocal 기반 상태 관리까지 정리해보겠습니다.


1. Task란 무엇인가?

  • Task비동기적으로 실행되는 작업 단위입니다.
  • 스레드를 직접 다루지 않고, Swift 런타임이 알아서 스케줄링합니다.
  • Task는 계층 구조를 가지며(부모-자식 관계), 구조화된(concurrent tree) 실행이 가능합니다.

2. Task 생성 방법

기본 Task

Task {
    await fetchData()
}
  • 현재 컨텍스트를 승계합니다 (우선순위, Actor, TaskLocal).
  • 비구조화(Unstructured) Task이므로 명시적으로 관리가 필요합니다.

Detached Task

Task.detached {
    await heavyBackgroundWork()
}
  • 부모 컨텍스트를 승계하지 않습니다.
  • 완전히 독립된 작업으로 실행됩니다.
  • MainActor 보장 없음 → UI 작업에는 사용하면 안 됩니다.

async let

async let a = fetchUser()
async let b = fetchPosts()
let result = await (a, b)
  • 부모 Task와 수명이 연결됩니다.
  • 스코프가 종료되면 자동으로 취소됩니다.
  • 소수의 병렬 작업을 실행할 때 유용합니다.

withTaskGroup

try await withThrowingTaskGroup(of: Data.self) { group in
    for url in urls {
        group.addTask { try await fetch(url) }
    }
    for try await data in group {
        print("Fetched: \(data.count) bytes")
    }
}
  • 동적으로 많은 작업을 실행할 때 유용합니다.
  • 자식 Task의 에러가 자동으로 전파됩니다.

3. Task Priority (우선순위)

Swift의 Task는 실행 우선순위를 설정할 수 있습니다.
이는 GCD의 QoS(Quality of Service)와 유사하며, 시스템이 스케줄링할 때 참고하는 힌트 역할을 합니다.

주요 우선순위 종류

  • .high

    • 가장 높은 우선순위. 시스템 리소스를 최우선적으로 확보해야 할 때 사용합니다.
    • 일반적으로 직접 쓰는 경우는 드뭅니다.
  • .userInitiated

    • 사용자의 직접적인 요청에 의해 즉시 실행되어야 하는 작업.
    • 예: 버튼을 눌렀을 때 데이터 로딩, 화면 전환 등.
    • 가장 자주 쓰이는 우선순위.
  • .utility

    • 즉각적이지 않지만, 일정 시간 내 완료되면 좋은 작업.
    • 예: 파일 다운로드, 데이터 동기화, 백그라운드 분석.
    • CPU와 배터리 사용량을 적절히 조절.
  • .background

    • 사용자에게 즉시 보이지 않는 작업.
    • 예: 캐시 정리, 로그 업로드, 원격 데이터 사전 로드.
    • 리소스 사용이 가장 낮음.
  • .low

    • 거의 사용하지 않는 우선순위.
    • 최저 우선순위로, 리소스가 남을 때만 실행됩니다.

사용 예시

let task = Task(priority: .userInitiated) {
    await loadUserData()
}
  • 👉 주의: 우선순위는 힌트일 뿐 절대적인 보장은 없습니다.
  • 시스템 상황(iOS의 전력 관리, 스케줄링 정책 등)에 따라 달라질 수 있습니다.

4. 취소(Cancellation)

  • Swift의 Task 취소는 협력적(coperative)입니다.
  • 즉, 직접 체크하지 않으면 취소되지 않습니다.
if Task.isCancelled { return }
try Task.checkCancellation()

또는

try await withTaskCancellationHandler {
    try await longIO()
} onCancel: {
    cleanup()
}

5. SwiftUI와 Task 결합 패턴

View에서 .task 사용

.task {
    items = try await api.fetchItems()
}

ViewModel에서 Task 관리

@MainActor
final class FeedViewModel: ObservableObject {
    @Published var feeds: [Feed] = []
    private var task: Task<Void, Never>?

    func start() {
        task?.cancel()
        task = Task {
            while !Task.isCancelled {
                feeds = try await api.fetchFeeds()
                try await Task.sleep(nanoseconds: 5_000_000_000)
            }
        }
    }

    deinit { task?.cancel() }
}

6. 네트워크/데이터 처리 예제

async/await 네트워크 호출

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://jsonplaceholder.typicode.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

동시 병렬 처리

async let profile = api.fetchProfile()
async let banners = api.fetchBanners()
async let recommends = api.fetchRecommends()
let home = try await HomeModel(profile: profile, banners: banners, recommends: recommends)

7. Actor

actor Counter {
    private var value = 0

    func increment() {
        value += 1
    }

    func current() -> Int { value }
}
  • Actor는 데이터 경쟁(Data Race)을 원천 차단합니다.

8. MainActor 와 Task

@MainActor
class MyViewModel: ObservableObject {
    @Published var text = ""

    func load() async {
        let data = await fetchText()
        text = data
    }
}
  • @MainActor로 선언하면 UI 업데이트를 항상 안전하게 실행할 수 있습니다.
  • Task.detached는 MainActor 승계를 하지 않으므로, UI 접근 시 충돌이 발생할 수 있습니다.

9. TaskLocal

enum Trace {
    @TaskLocal static var requestID: String = ""
}

await Trace.$requestID.withValue("abcd-1234") {
    await doSomething()
    print("Current ID:", Trace.requestID)
}
  • 하위 Task에 컨텍스트 값을 전파할 수 있습니다.

10. 언제 무엇을 써야 할까?

  • 병렬로 몇 개만: async let
  • 많고 동적: withTaskGroup
  • 뷰와 수명 분리된 작업: Task {}
  • 정말 독립적이어야 할 때: Task.detached (가능하면 지양)

11. 반패턴 주의

  • 무분별한 Task.detached → Actor 격리 깨짐, 데이터 레이스 위험
  • UI 업데이트를 MainActor 아닌 곳에서 수행
  • async let 선언 후 await 누락
  • 무한 루프 안에서 Task.isCancelled 체크 누락

12. Best Practice 요약

  • UI 업데이트: 반드시 @MainActor 보장
  • 공유 상태 관리: Actor 사용 → 데이터 경쟁 차단
  • 로그/세션 전파: TaskLocal 활용
  • 병렬 작업: async let (소수) / withTaskGroup (다수, 동적)
  • 취소 처리: Task.isCancelled, Task.checkCancellation()
  • 우선순위: .userInitiated.utility 중심으로, 상황에 맞게 설정

13. 정리

  • Task: Swift Concurrency의 실행 단위
  • Actor: 동시성에서 안전한 상태 관리
  • MainActor: UI 업데이트 안전 보장
  • TaskLocal: Context 전파 도구
  • Priority: 실행 시급성에 따른 힌트 제공
profile
iOS 개발자 공부하는 Roy

0개의 댓글