---
id: ios-swift-concurrency-actor-patterns
title: Swift Concurrency — Actor / @MainActor / Sendable
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [ios, swift, actor, concurrency, vibe-coding]
tech_stack: { language: "Swift", applicable_to: ["iOS"] }
applied_in: []
aliases: [actor, @MainActor, Sendable, isolation, data race, Swift 6]
---

# Swift Concurrency — Actor

> Race condition 없는 mutable 상태 = **actor**. UI = `@MainActor`. **Sendable** 로 thread-safe 표시. Swift 6 의 strict concurrency 가 컴파일 시 데이터 race 잡음.

## 📖 핵심 개념
- Actor: 한 번에 한 코드만 실행. Internal 상태 격리.
- @MainActor: main thread 에서만.
- Sendable: 다른 actor 로 안전하게 전달 가능.
- nonisolated: actor 안에서도 격리 X (read-only).

## 💻 코드 패턴

### Actor 기본
```swift
actor BankAccount {
    private var balance: Decimal = 0

    func deposit(_ amount: Decimal) {
        balance += amount
    }

    func withdraw(_ amount: Decimal) throws -> Decimal {
        guard balance >= amount else { throw InsufficientFunds() }
        balance -= amount
        return amount
    }

    var current: Decimal { balance }
}

let acc = BankAccount()
await acc.deposit(100)
let bal = await acc.current  // await 필요 (cross-actor)
```

### @MainActor (UI)
```swift
@MainActor
class ViewModel: ObservableObject {
    @Published var users: [User] = []

    func load() async {
        let r = try? await api.fetchUsers()
        users = r ?? [] // main thread 에서 자동 실행
    }
}

// 호출
Task { await viewModel.load() }
```

### nonisolated
```swift
actor Logger {
    private var logs: [String] = []
    func add(_ msg: String) { logs.append(msg) }

    nonisolated var name: String { "AppLogger" } // 동기 read OK
}
```

### Sendable
```swift
struct User: Sendable {  // 명시
    let id: UUID
    let name: String
}

// final class 도 Sendable 가능 (모든 프로퍼티 immutable)
final class UserConfig: Sendable {
    let theme: String
    init(theme: String) { self.theme = theme }
}

// @unchecked Sendable: 컴파일러 검사 우회 (잠금 직접 관리)
final class Cache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: String] = [:]
    func set(_ k: String, _ v: String) {
        lock.lock(); defer { lock.unlock() }
        storage[k] = v
    }
}
```

### Task / async let
```swift
func loadDashboard() async throws -> Dashboard {
    async let user = api.user()
    async let orders = api.orders()
    async let ads = api.ads()
    return try await Dashboard(user: user, orders: orders, ads: ads)
}
```

### TaskGroup (동적 병렬)
```swift
func loadImages(urls: [URL]) async throws -> [URL: Data] {
    try await withThrowingTaskGroup(of: (URL, Data).self) { group in
        for url in urls {
            group.addTask {
                let (data, _) = try await URLSession.shared.data(from: url)
                return (url, data)
            }
        }
        var result: [URL: Data] = [:]
        for try await (url, data) in group {
            result[url] = data
        }
        return result
    }
}
```

### Cancellation
```swift
let task = Task {
    while !Task.isCancelled {
        try Task.checkCancellation()
        await tick()
        try await Task.sleep(for: .seconds(1))
    }
}

task.cancel()
```

### Continuation (callback → async)
```swift
func legacyFetch() async throws -> Data {
    try await withCheckedThrowingContinuation { cont in
        legacyAPI.load { result in
            switch result {
            case .success(let data): cont.resume(returning: data)
            case .failure(let e): cont.resume(throwing: e)
            }
        }
    }
}
```

### Global actor
```swift
@globalActor actor DatabaseActor {
    static let shared = DatabaseActor()
}

@DatabaseActor
class UserRepository {
    func save(_ u: User) { ... } // 모두 DatabaseActor 안
}
```

### Swift 6 strict concurrency
```swift
// SwiftSettings:
// .enableExperimentalFeature("StrictConcurrency")
// 또는 -strict-concurrency=complete

// 데이터 race 컴파일 시 에러
```

## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Mutable shared state | actor |
| UI / ViewModel | @MainActor |
| Pure data | struct + Sendable |
| Legacy callback | continuation |
| 병렬 fetch | async let |
| Dynamic 병렬 | TaskGroup |
| 시간 의존 작업 | Task + sleep + cancel |

## ❌ 안티패턴
- **Actor 안 외부 mutable class 보유**: race 가능. 그 객체도 actor / Sendable.
- **`@MainActor` 없는 UI 변경**: warning + 잘못된 thread 실행.
- **`Task.detached` 무절제**: priority / context 잃음.
- **await 안 catch — Task 안 throw**: silent fail.
- **Cancellation 검사 없는 long task**: cancel 무시.
- **Dispatch + async 혼용**: 같은 코드에 둘 — 혼란. 선택.
- **Continuation 두 번 resume**: crash.
- **`@unchecked Sendable` 남발**: 검사 우회 = 데이터 race 잠재.

## 🤖 LLM 활용 힌트
- UI = @MainActor / 데이터 store = actor.
- Sendable struct 디폴트.
- Swift 6 strict concurrency 켜기.

## 🔗 관련 문서
- [[iOS_Swift_Concurrency_async_await]]
- [[iOS_Swift_Memory_ARC_Cycles]]
- [[iOS_SwiftUI_State_Property_Wrappers]]
