Swift Concurrency 정리 (Swift 6 · iOS 18 · Xcode 26)
Swift 6의 actor, Task, TaskGroup, 취소(cancellation), @MainActor 등 동시성 도구를 단계별로 정리합니다. 코드 예제와 함께 실제로 어떻게 쓰는지 바로 복사해서 붙여넣을 수 있도록 구성했습니다.
⸻
전제(환경)
• Swift 6 이상
• iOS 18 이상
• Xcode 26 이상
⸻
들어가기 전 — 비유로 빠르게 이해하기
앱을 한 명의 바텐더라고 생각하면 이해하기 쉽습니다. 여러 손님 주문을 동시에 처리해야 하고, 전화도 오고, 새 음료도 만들어야 합니다. 동시성은 앱이 여러 작업을 ‘겉보기상 동시에’ 효율적으로 처리하게 해주지만, 손님끼리 음료가 뒤섞이면 안 되므로 데이터 소유권과 동기화가 중요합니다.
⸻
⸻
actor Counter {
private var value: Int = 0
func increment() {
value += 1
}
func get() -> Int {
return value
}
}
사용 예:
let counter = Counter()
Task {
await counter.increment()
let v = await counter.get()
print("counter = \(v)")
}
설명: actor는 내부의 가변 상태에 대한 동시 접근을 자동으로 직렬화한다. 외부에서 바로 상태를 변경할 수 없으므로 데이터 경쟁을 방지한다.
⸻
func fetchData(from url: URL) async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
Task {
do {
let data = try await fetchData(from: URL(string: "https://example.com")!)
print("data size: \(data.count)")
} catch {
print("fetch error:", error)
}
}
설명: await는 비동기 작업이 끝날 때까지 현재 Task를 일시 중단한다. Task {} 내부에서 await를 사용할 수 있다.
⸻
func fetchAll(_ urls: [URL]) async throws -> [Data] {
return try await withThrowingTaskGroup(of: Data?.self) { group in
for url in urls {
group.addTask {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
}
var results = [Data]()
for try await data in group {
if let d = data { results.append(d) }
}
return results
}
}
설명: withThrowingTaskGroup를 사용하면 자식 Task의 생명주기를 부모가 관리하므로 에러/취소 전파가 일관된다. 작업이 완료되는 순서대로 결과를 처리한다.
⸻
func performLongOperation() async throws {
try Task.checkCancellation() // 시작 즉시 취소 확인
let handle = openSomeResource()
defer { handle.close() } // 취소되어도 리소스 정리
for i in 0..<1000 {
try Task.checkCancellation() // 루프 중간중간 취소 확인
// 긴 작업 수행...
}
}
설명: Task.checkCancellation()은 취소 시 CancellationError를 던져 빠르게 종료한다. defer로 리소스 정리를 보장한다.
⸻
@MainActor
class ViewModel: ObservableObject {
@Published private(set) var items: [String] = []
func load(urls: [URL]) {
Task {
do {
let dataList = try await fetchAll(urls)
items = dataList.map { String(data: $0, encoding: .utf8) ?? "" }
} catch {
// 에러 처리
}
}
}
}
설명: @MainActor를 적용하면 ViewModel의 상태 변경이 메인 스레드에서 안전하게 일어난다. UI 바인딩이 안전해진다.
⸻
@MainActor
final class ImageService {
private var inFlight = [URL: Task<UIImage?, Error>]()
func image(for url: URL) async throws -> UIImage? {
if let task = inFlight[url] {
return try await task.value
}
let task = Task {
defer { Task { await removeInFlight(url: url) } }
let (data, _) = try await URLSession.shared.data(from: url)
return UIImage(data: data)
}
inFlight[url] = task
return try await task.value
}
private func removeInFlight(url: URL) {
inFlight.removeValue(forKey: url)
}
}
설명: 동일 URL에 대한 기존 Task가 있으면 재사용해 네트워크/CPU 낭비를 줄인다.
⸻
⸻
⸻
⸻
결론
동시성 도구를 적절히 사용하면 여러 작업을 안전하고 효율적으로 처리할 수 있습니다. actor로 상태 소유권을 명확히 하고, Task/TaskGroup으로 작업 라이프사이클을 구조화하며, 취소와 메인 스레드 접근을 일관되게 처리하면 안정성과 성능이 개선됩니다. 위 코드를 직접 타이핑해 실행해 보면 개념이 더 빨리 체화됩니다.