"단일 스레드로 시작해. 빠져나갈 거면 일부러 빠져나가고, 그걸 타입에 적어."
Swift 6.2에서 바뀐 것
2025년 9월에 나온 Swift 6.2는 Swift 6에 대한 제일 흔한 불만에 답했어. 진짜 병렬 처리도 없는 작은 앱이 격리 에러에 파묻힌다는 불만이었지. 그래서 서로 이어진 세 가지를 넣었고, 이걸 묶어서 approachable concurrency라고 불러.
- 기본 액터 격리. 모듈 하나가 "격리를 따로 안 적은 선언은 전부
@MainActor"라고 선언할 수 있어. SwiftPM에선.defaultIsolation(MainActor.self), 컴파일러 명령줄에선-default-isolation MainActor야. 예외는 상식적이야. actor, 격리를 명시한 선언, 슈퍼클래스나 프로토콜에서 격리를 물려받는 선언은 원래 격리를 그대로 유지해. nonisolated(nonsending). 6.2 전엔nonisolated async함수가 무조건 전역 동시 실행자로 넘어갔어.NonisolatedNonsendingByDefault예정 기능을 켜면 그 대신 호출한 쪽의 격리에서 돌아. 메인 액터에서 부르면 메인 액터에 그대로 머물러.@concurrent. 명시적인 출구야.@concurrent가 붙은 함수는 누가 불렀든 그 액터를 떠나 동시 실행자에서 돌아. 무거운 디코딩, 이미지 처리, 스캔에 이걸 눈에 보이게 붙여.
Xcode 26은 새 앱 프로젝트에 이걸 켜놔. SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, SWIFT_APPROACHABLE_CONCURRENCY = YES. 기존 프로젝트는 네가 바꾸기 전까지 옛 설정을 유지해.
경고 스위치가 아니라 뜻이 바뀌는 설정이야
기존 모듈에서 기본 격리를 뒤집으면 코드의 뜻이 바뀌어. 격리 안 돼 있던 헬퍼가 메인 액터에 격리되니까, 백그라운드에서 부르는 쪽은 이제 await를 붙여야 해. nonisolated(nonsending)을 켜면 비동기 코드가 어디서 도는지가 바뀌어. 메인 액터를 떠날 줄 알았던 async 함수가 이제 거기 머물러. 그래서 그 안의 긴 동기 구간은 @concurrent를 붙이기 전까진 UI를 막아. 둘 다 호출하는 곳에선 티가 안 나. 언어 모드를 바꿀 때만큼 조심해서, 타깃마다 마이그레이션으로 다뤄.
가족은 이렇게 받아들여
가족 패키지는 이 기본값이 생기기 전에 쓰여서 격리를 다 적어뒀어. UI 타입엔 @MainActor, 공유 상태엔 actor, 프레임워크 콜백이 오는 곳엔 nonisolated랑 @Sendable. 그 방식은 6.2에서도 여전히 맞아. UI가 많은 새 앱 타깃(Spark의 Mac 앱이나 아이폰 앱처럼)이면 메인 액터 기본 격리가 현실에 맞아. 거의 모든 코드가 UI를 건드리니까. 앱, 확장, 워치 앱이 같이 쓰는 공유 코어 라이브러리는 격리 없는 기본값과 명시적 어노테이션을 그대로 유지해. 여러 문맥에서 도는 코드라서, 슬그머니 한 스레드에 묶이면 안 되거든.