Swift 5.5부터 도입된 Concurrency(비동기/동시성)는 기존의 GCD나 Operation보다 훨씬 안전하고 직관적인 코드를 작성할 수 있게 해줍니다.
그 중심에는 바로 Task
라는 개념이 있습니다. 이번 글에서는 Task
를 중심으로 Swift Concurrency
의 핵심과 함께, SwiftUI와의 결합 패턴, 네트워크/데이터 처리 Best Practice, 그리고 Actor
/ MainActor
/ TaskLocal
기반 상태 관리까지 정리해보겠습니다.
Task
는 비동기적으로 실행되는 작업 단위입니다.Task
는 계층 구조를 가지며(부모-자식 관계), 구조화된(concurrent tree) 실행이 가능합니다.Task {
await fetchData()
}
Task.detached {
await heavyBackgroundWork()
}
async let a = fetchUser()
async let b = fetchPosts()
let result = await (a, b)
Task
와 수명이 연결됩니다. 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")
}
}
Swift의 Task
는 실행 우선순위를 설정할 수 있습니다.
이는 GCD의 QoS(Quality of Service)와 유사하며, 시스템이 스케줄링할 때 참고하는 힌트 역할을 합니다.
.high
.userInitiated
.utility
.background
.low
let task = Task(priority: .userInitiated) {
await loadUserData()
}
if Task.isCancelled { return }
try Task.checkCancellation()
또는
try await withTaskCancellationHandler {
try await longIO()
} onCancel: {
cleanup()
}
.task {
items = try await api.fetchItems()
}
@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() }
}
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)
actor Counter {
private var value = 0
func increment() {
value += 1
}
func current() -> Int { value }
}
@MainActor
class MyViewModel: ObservableObject {
@Published var text = ""
func load() async {
let data = await fetchText()
text = data
}
}
@MainActor
로 선언하면 UI 업데이트를 항상 안전하게 실행할 수 있습니다.Task.detached
는 MainActor 승계를 하지 않으므로, UI 접근 시 충돌이 발생할 수 있습니다.enum Trace {
@TaskLocal static var requestID: String = ""
}
await Trace.$requestID.withValue("abcd-1234") {
await doSomething()
print("Current ID:", Trace.requestID)
}
async let
withTaskGroup
Task {}
Task.detached
(가능하면 지양)Task.detached
→ Actor 격리 깨짐, 데이터 레이스 위험async let
선언 후 await
누락Task.isCancelled
체크 누락@MainActor
보장 async let
(소수) / withTaskGroup
(다수, 동적) Task.isCancelled
, Task.checkCancellation()
.userInitiated
와 .utility
중심으로, 상황에 맞게 설정