Swift 6 컴파일러는 기본 설정으로 완전한 동시성 검사(Complete Concurrency Checking)를 강제합니다.
기존 Swift 5 스타일의 비동기 코드나 DispatchQueue.main.async에 의존하던 로직은 컴파일 시점에 Call to main actor-isolated instance method in a synchronous nonisolated context 또는 Capture of non-sendable type in a concurrent closure 에러를 발생시킵니다. 데이터 레이스(Data Race) 문제를 해결하는 실무 예제와 패턴을 정리합니다.
1. MainActor 격리(Isolation) 및 UI 업데이트 실무 패턴
Swift 6에서는 UI 렌더링에 관여하는 모든 타입과 함수가 메인 스레드에서 안전하게 실행됨을 보장해야 합니다.
@MainActor 클래스 및 ViewModel 격리
ObservableObject 또는 @Observable 마크업을 사용하는 ViewModel 클래스 전체에 @MainActor를 선언하면 내부 상태 변경이 메인 스레드에서 캡슐화됩니다.
import SwiftUI
@MainActor
final class UserProfileViewModel: ObservableObject {
@Published private(set) var userName: String = ""
@Published private(set) var isLoading: Bool = false
private let networkService: NetworkServiceProtocol
init(networkService: NetworkServiceProtocol) {
self.networkService = networkService
}
func fetchUserProfile() async {
isLoading = true
defer { isLoading = false }
do {
let profile = try await networkService.loadProfile()
self.userName = profile.name
} catch {
print("프로필 로드 실패: \(error)")
}
}
}
nonisolated 키워드 활용
@MainActor가 지정된 클래스 내부에서도 UI 업데이트와 상관없는 무거운 순수 연산 함수는 nonisolated 키워드를 붙여 메인 스레드 병목 현상을 방지합니다.
@MainActor
final class DataProcessor {
// UI 업데이트용 프로퍼티
var statusText: String = ""
// 메인 스레드를 블로킹하지 않는 순수 대용량 데이터 계산 함수
nonisolated func processLargeDataSet(_ data: [Int]) -> Int {
return data.reduce(0, +)
}
}
2. Sendable 프로토콜과 동시성 상태 전달 규칙
Sendable 프로토콜은 서로 다른 스레드/태스크 간에 안전하게 전달될 수 있는 타입임을 컴파일러에 증명하는 키워드입니다.
Sendable 적용 기준 비교
| 구분 | Struct / Enum | Class | Actor |
| Sendable 자동 채택 | 내부 프로퍼티가 모두 Sendable인 경우 자동 | 자동 채택 안 됨 (명시 필요) | 기본적으로 Sendable 준수 |
| 가변 상태 (Mutable State) | 불변 구조체 및 값 타입 안전 | 불변(final & let) 전제하에만 제약적 허용 | internal state 보호를 통해 안전 |
| 권장 가이드 | DTO, 모델 객체는 값 타입 적극 활용 | 불필요한 레퍼런스 타입 최소화 | 상태 관리는 Class 대신 Actor 사용 |
클래스에 @unchecked Sendable 적용 시 주의사항
내부적으로 커스텀 락(NSLock 등)을 사용하여 스레드 안전성을 스스로 보장하는 경우에만 @unchecked Sendable을 채택합니다.
// 커스텀 스레드 세이프 클래스 예시
final class CustomSafeCache: @unchecked Sendable {
private var storage: [String: Any] = [:]
private let lock = NSLock()
func getValue(forKey key: String) -> Any? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
}
3. 기존 콜백 기반 코드를 async/await로 마이그레이션
기존 Completion Handler 및 DispatchQueue 기반 비동기 코드는 withCheckedThrowingContinuation을 사용해 Swift Concurrency로 안전하게 전환할 수 있습니다.
func fetchLegacyData() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
legacySDKFetch { result, error in
if let error = error {
continuation.resume(throwing: error)
} else if let data = result {
continuation.resume(returning: data)
} else {
continuation.resume(throwing: URLError(.badServerResponse))
}
}
}
}
요약
Swift 6 데이터 레이스 에러는 UI 조작부에 @MainActor를 명시하고, 태스크 간 전송되는 모델 데이터에 Sendable 프로토콜을 채택함으로써 컴파일 시점에 스레드 안전성을 확보할 수 있습니다.
'개발 > Swift' 카테고리의 다른 글
| Swift 6 렌더링 최적화: ObservableObject에서 @Observable 마이그레이션 가이드 (0) | 2026.08.20 |
|---|---|
| Swift 6 Strict Concurrency 대응: SwiftData ModelActor 비동기 파싱 및 저장 가이드 (0) | 2026.08.05 |
| Xcode 27 / Swift 6.3 SPM 동적 라이브러리(Dynamic Framework) 링킹 충돌 원인과 해결 방법 (0) | 2026.08.04 |
| TCA Study - State · Action · Reducer · Store — TCA의 핵심 구조 완전 정복 (0) | 2025.12.16 |
| TCA Study - Swift 개발자가 TCA를 알아야 하는 이유 (0) | 2025.12.16 |
| Swift에서 정렬: sort vs sorted (0) | 2025.11.11 |
| Swift에서 옵셔널 기본값: 중첩 if let vs a ?? b (0) | 2025.11.11 |
| Swift에서 배열 초기화: 반복 append vs Array(repeating:count:) (0) | 2025.11.11 |


