Swift - Task

Marble·2026년 3월 4일

Concurrency

목록 보기
6/10

저번에 async/await 관한 글을 쓸 때 Task에 대해 간략히 말했었습니다. 그래서 이번 글에서는 Task에 대해 알아볼게요

Task

사용방법

Task는 비동기 작업 하나를 나타내는 타입입니다. 동기 컨텍스트 어디서든 만들 수 있습니다.

  Button("불러오기") {
      // 여기는 동기 컨텍스트
      Task {
          // 여기는 비동기 컨텍스트
          await viewModel.load()
      }
  }

반환값에 접근하고 싶다면 await으로 기다릴 수 있습니다.

  let task = Task {
      return try await fetchUser(id: 42)
  }

  let user = try await task.value

Unstructured Concurrency

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)
  }
  • 부모 Task가 취소되면 자식도 자동 취소되며 에러 또한 자동으로 전파됩니다.
  • 모든 자식이 끝나야 부모가 종료됩니다.
  • async let, TaskGroup이 여기 속합니다.

Unstructured(비구조적) concurrency는 scope와 독립적으로 살아있는 방식입니다.

func onButtonTap() {
      Task { await doSomething() } // scope를 벗어나도 계속 실행
}
  • 현재 흐름과 분리되어 있기 때문에 취소/에러가 자동으로 전파 안 되며,
  • 부모 Task가 없으며(또는 무시),
  • Task { }, Task.detached { } 가 여기 속합니다.

Task의 생명주기와 취소

위에서 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.detached

아까 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 상태를 건드리게 될 수도 있기 때문에 사용할 때 다음 방식을 권장합니다.

  1. await MainActor.run으로 명시적으로 전환
Task.detached(priority: .background) {
    let result = await heavyWork() // 백그라운드에서 처리

    await MainActor.run {
        self.state = result // Main Thread에서 안전하게 접근
    }
}
  1. @MainActor 함수로 분리
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 상속OX
Priority 상속OX
@MainActor 클래스 안에서 self 접근안전위험
해결책없어도 됨MainActor.run 또는 @MainActor 함수 분리
주요 용도일반적인 비동기 작업Main Actor 밖에서 실행할 무거운 작업
profile
개발자가 되고 싶은 공돌이

0개의 댓글