← 모든 글

실전에서 쓰는 Swift 6.2 동시성: 기본값을 MainActor로, 벗어날 때는 의도적으로

Swift 6.0은 데이터 경쟁을 컴파일 오류로 만들었고, 그 결과 1년 동안 모두가 그 대가를 치렀습니다. strict-concurrency 검사기는 평범한 UI 코드를 Sendable 위반의 벽과 “main actor-isolated property can not be referenced from a nonisolated context” 오류 더미로 바꿔 놓았습니다. 진단 자체는 옳았지만(그 코드는 실제로 경쟁할 수 있었습니다) 그 양이 신호를 묻어 버렸고, 많은 팀은 Swift 5 모드에 머무르거나 오류가 잠잠해질 때까지 @MainActor를 여기저기 뿌리는 쪽을 택했습니다.

Swift 6.2는 규칙이 아니라 기본값을 바꿉니다. 데이터 경쟁 안전성 보장은 동일하며, 바뀐 것은 컴파일러의 출발점입니다. 새 기본값을 채택하면 벽의 대부분이 사라집니다. 이제 컴파일러가 여러분의 앱에서 이미 참이었던 사실을 전제하기 때문입니다. 즉 코드의 대부분은 메인 액터에서 돌아가고, 거기서 벗어나는 것은 의도적으로, 이름 붙은 자리에서만 한다는 사실 말입니다. 이것이 제가 941 패밀리 앱 전반에 적용하는 모델이며1, What’s New in Swift 2026 정리가 뒤이은 6.3 및 6.4 제안들을 통해 짚어 가는 더 큰 진화 흐름의 한 조각이기도 합니다. 그 작동 방식과, 이를 켠 뒤에도 여전히 물어뜯는 여섯 가지 오류를 살펴봅니다.

TL;DR

  • SE-0466은 모듈 전체의 기본값을 @MainActor로 둘 수 있게 합니다. SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor를 설정하면 모든 뷰, 모델, 뷰 모델에 일일이 어노테이션을 달 필요가 없어집니다2.
  • SE-0461은 nonisolated async 함수를 기본적으로 호출자의 액터에서 실행합니다(nonisolated(nonsending)). 그래서 async 코드를 호출해도 더 이상 액터 홉이 강제되지 않으며, 그에 따라오던 경계를 넘는 Sendable 오류도 사라집니다3.
  • @concurrent는 탈출구입니다. 함수에 @concurrent를 붙이면 무거운 CPU 작업(디코딩, 이미지 처리)을 의도적으로, 눈에 보이게 백그라운드 스레드로 밀어낼 수 있습니다4.
  • Xcode 26은 새 프로젝트에 대해 두 기본값을 모두 켭니다. 이를 아우르는 스위치는 SWIFT_APPROACHABLE_CONCURRENCY = YES입니다5.
  • 이 모델은 부담을 뒤집습니다. 모든 줄이 메인 밖에서 돌아도 안전하다는 것을 증명하는 대신, 모든 것을 메인 위에 두고 벗어나는 몇 군데만 증명합니다.
  • 전환 후에도 구체적인 오류 여섯 가지가 살아남습니다. 여섯 가지 모두 패턴이 보이고 나면 한 줄로 고칠 수 있습니다.

역전, 그리고 그것이 중요한 이유

옛 모델(Swift 6.0)에서 코드는 격리하기 전까지는 nonisolated였습니다. UI 상태를 건드리는 모든 타입에 @MainActor가 필요했고, 격리 경계를 넘는 모든 async 호출에 Sendable 준수가 필요했으며, 컴파일러는 그 빈틈을 하나하나 지적했습니다. 코드의 95퍼센트가 이미 메인 스레드에서 돌아가는 앱이라면, 의심할 여지조차 없던 사실을 기술하기 위해 그 95퍼센트에 어노테이션을 다는 데 시간을 썼던 셈입니다.

새 모델(Swift 6.2)에서 코드는 거기서 벗어나기 전까지는 메인 액터 위에 있습니다. SE-0466은 메인 액터 격리를 모듈 기본값으로 선언할 수 있게 해 주므로, 뷰와 그 모델, 헬퍼가 어노테이션 하나 없이 모두 @MainActor가 됩니다2. 그다음 SE-0461이 두 번째 세금을 없앱니다. 이제 nonisolated async 함수는 글로벌 익스큐터로 홉하는 대신 자신을 호출한 액터 위에서 돌아가므로, 이를 await해도 격리 경계를 넘어 끌려가거나 스코프 안의 모든 것에 Sendable을 요구받지 않습니다3.

이 멘탈 모델은 UI 앱이 실제로 어떻게 동작하는지와 일치합니다. 기본적으로 메인 스레드라는 것은 타협이 아니라, 상태가 곧 뷰인 앱의 진실입니다. 동시성은 모든 줄에서 방어해야 하는 주변 조건이 아니라, 이름이 붙고 가둬진, 손을 뻗어 쓰는 예외가 됩니다. 컴파일러의 일은 “이것을 동시에 돌려도 안전함을 증명하라”에서 “당신이 이것은 동시에 돈다고 했으니, 안전함을 증명하라”로 뒤집히고, 두 번째 질문은 훨씬 적은 곳에서만 던져집니다.

Swift 코어 팀도 같은 말을 했습니다. WWDC 2026 Swift Group Lab에서 Holly는, 오늘 다시 동시성 모델을 설계한다면 nonisolated async 함수가 첫날부터 호출자의 컨텍스트에서 돌아갔을 것이라고 설명했습니다. 그녀는 자신이 오랫동안 글로벌 동시성 풀 모델을 고수했지만 실제 프로젝트에서 일어난 문제들 때문에 생각을 바꾸게 되었다고 풀어서 말했습니다. Doug는 옛 기본값이 너무 많은 타입을 Sendable 쪽으로 밀어붙였다고 덧붙였습니다. (두 발언 모두 로컬에서 전사한 랩 녹음을 풀어 옮긴 것입니다. Apple은 이 세션에 대한 캡션을 공개하지 않았습니다.)9

켜는 방법

빌드 설정 두 개입니다. 둘 다 Xcode 26의 새 프로젝트 기본값이며, 기존 프로젝트에서도 명시적으로 설정할 가치가 있습니다5.

SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES

첫 번째는 SE-0466으로, 모듈의 기본값을 메인 액터 격리로 만듭니다. 두 번째는 SE-0461의 “호출자에서 실행” 동작을 포함한 approachable-concurrency 기능 묶음을 켜는 포괄 설정입니다. Swift 패키지에서는 Xcode 빌드 설정 대신 그에 맞는 upcoming-feature 플래그를 통해 swiftSettings에서 같은 기본값을 설정합니다6.

기존 프로젝트에서 둘 다 켜면 오류 수가 크게 떨어집니다. 검사기가 지적하던 것 대부분이, 이전에는 메인 스레드라고 전제할 수 없던 메인 스레드 코드였기 때문입니다. 남는 것은 진짜 경계 사례들의 짧은 목록입니다. 그것들은 이름으로 알아 둘 가치가 있습니다. 각각이 여러분의 코드가 실제로 메인 액터에서 벗어나는 자리이고, 그 수정이란 그 사실을 정확히 명시하는 일이기 때문입니다.

전환 후에도 살아남는 여섯 가지 오류

다음은 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 아래에서도 여전히 나타나는 strict-concurrency 오류로, 941 패밀리 앱을 이 모델로 이전하며 추려낸 것들입니다1. 모두 거짓 양성이 아니라 진짜 경계입니다. 그래서 수정은 억제가 아니라 정확한 어노테이션이 됩니다.

1. Sendable 타입의 순수 함수. Sendable한 enum의 순수 메서드(URL 빌더, 포매터)는 모듈 기본값 아래에서 메인 액터 격리를 상속한 다음, nonisolated 컨텍스트에서 호출되면 오류를 냅니다. “Call to main actor-isolated instance method in a synchronous nonisolated context.” 이 메서드는 어떤 상태도 건드리지 않으므로 메인 액터에 격리하는 것은 잘못입니다. nonisolated를 붙이세요7.

nonisolated func searchURL(for query: String) -> URL? { ... }

2. 기본 매개변수 안의 싱글턴 static. static let shared = Foo()는 기본값 아래에서 메인 액터 격리되지만, 기본 매개변수 값은 호출자의 컨텍스트에서 평가되며 그것은 흔히 nonisolated입니다. “Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context.” 이 static을 nonisolated로 만드세요. 타입이 Sendable이라면(혹은 상태를 직접 지키기에 @unchecked Sendable이라면) unsafe 한정자는 필요 없습니다.

final class KeychainProxySecretStore: @unchecked Sendable {
    nonisolated static let shared = KeychainProxySecretStore()
}

3. 불변 원시 상수. 같은 기본 매개변수 문제가 평범한 상수에도 적용됩니다. nonisolated 기본 인수에서 참조되는 static let defaultInterval입니다. 수정은 동일하며, 이 상수는 공유해도 자명하게 안전합니다.

nonisolated static let defaultInterval: TimeInterval = 15 * 60

4. 캡처한 self를 읽는 Task 본문. 바깥 클로저가 [weak self]를 캡처하고, 그 안에서 Task { @MainActor in self?.foo() }가 그 캡처된 옵셔널을 읽습니다. “Reference to captured var ‘self’ in concurrently-executing code.” 이 Task는 둘러싼 스코프의 var 바인딩을 동시에 읽고 있으며, 이것이 경쟁입니다. Task 경계에서 self를 다시 캡처하여 Task가 불변 바인딩을 소유하게 하세요.

NotificationCenter.default.addObserver(...) { [weak self] _ in
    Task { @MainActor [weak self] in
        self?.value = next
    }
}

5. 메인 액터 상태를 읽는 KVO 콜백. webView.observe(\.canGoBack) { wv, _ in ... } 콜백은 @Sendable이고 따라서 nonisolated이지만, WKWebView.canGoBack은 메인 액터 격리되어 있습니다. “Main actor-isolated property ‘canGoBack’ can not be referenced from a Sendable closure.” KVO는 값을 변경한 스레드에서 동기적으로 전달되고, WKWebView의 내비게이션 상태는 메인 스레드에서만 변경되므로 이 읽기는 건전합니다. MainActor.assumeIsolated로 그것을 단언하세요. 이는 Task 홉을 완전히 없애고 동기를 유지합니다8.

let pushNav: @Sendable (WKWebView?) -> Void = { [weak self] webView in
    MainActor.assumeIsolated {
        guard let self else { return }
        // safe to read webView?.canGoBack synchronously
    }
}

assumeIsolated는 컴파일러에게 던지는 질문이 아니라 약속입니다. 런타임 불변식이 정말로 성립하는 곳(문서화된 메인 스레드 콜백)에서만 쓰세요. 잘못된 약속은 경고가 아니라 크래시이기 때문입니다.

6. 메인에 있으면 안 되는 무거운 작업. 이것은 검사기가 지적하지 않으므로, 스스로 잡아내는 것이 가장 중요합니다. 메인 액터 기본값 아래에서는 동기식 CPU 바운드 메서드(큰 페이로드의 JSON 디코딩, 이미지 리사이즈)가 메인 액터 위에서 돌며 UI를 버벅이게 합니다. 기본값은 여러분을 메인 위에 묶어 둡니다. @concurrent는 의도적으로 벗어나는 방법입니다4.

@concurrent
func decodeLargePayload(_ data: Data) async throws -> Report {
    try JSONDecoder().decode(Report.self, from: data)
}

@concurrent는 함수를 글로벌 익스큐터로 내려보내며, 설계상 @MainActor, 커스텀 글로벌 액터, nonisolated(nonsending)와 상호 배타적입니다. 함수는 호출자가 있는 곳에서 돌거나 의도적으로 거기서 벗어나 돌거나 둘 중 하나이며, 결코 모호하지 않습니다4. 새 모델이 요구하는 규율은 전적으로 이 패턴 안에 있습니다. UI를 건드리는 모든 것은 메인에 두고, 측정상 백그라운드 스레드가 정말로 필요한 작업에 대해서만 @concurrent에 손을 뻗으세요.

기본값이 여러분에게 맞지 않을 때

기본값을 메인 액터로 두는 방식은 앱에 잘 맞습니다. SwiftUI와 UIKit 코드는 압도적으로 메인 스레드이고, 기본값이 현실과 일치합니다. 다음 두 경우에는 덜 맞으며, 아닌 척하는 것은 시간 낭비입니다.

  • UI가 없는 라이브러리나 프레임워크 타깃. 네트워킹 계층, 파서, 데이터 엔진은 기본값을 메인 액터로 둘 이유가 없으며, 그렇게 하면 거의 모든 것에 @concurrentnonisolated를 강요하게 됩니다. 그런 타깃에서는 SWIFT_DEFAULT_ACTOR_ISOLATION을 설정하지 않은 채 두고, 옛날 방식대로 의도적으로 격리하세요.
  • 액터 중심의 동시성 시스템. 설계가 정말로 많은 것을 병렬로 돌린다면(백그라운드 작업 몇 개를 가진 앱이 아니라 진짜 파이프라인이라면) 메인 액터 기본값은 여러분과 충돌합니다. 명시적인 액터와 nonisolated 코드가 필요하며, SE-0466 기본값은 잘못된 출발점입니다.

그렇지만 앱이라면 선택은 쉽습니다. 두 설정을 모두 켜고, 오류 수가 무너져 내리도록 둔 뒤, 남은 몇 개를 여러분의 코드가 메인 스레드에서 벗어나는 자리 그 자체의 지도로 다루세요. 그 지도는 가질 가치가 있습니다. 옛 모델은 천 개의 경고를 주되 지도는 주지 않았습니다. 새 모델은 정직한 경계 여섯 개와, 앱이 실제로 도는 방식과 마침내 일치하는 기본값을 줍니다.

노이즈와 신호에 관해 마지막으로 한마디. SourceKit은 Xcode가 인덱스를 다시 빌드하는 동안 에디터 안에서 파일을 넘나드는 인덱스 오류(“Cannot find type X in scope,” “No such module”)를 보여 줍니다. 특히 프로젝트를 다시 생성한 직후에 그렇습니다. 그것들은 인덱스 아티팩트이지 동시성 오류가 아닙니다. xcodebuild가 BUILD SUCCEEDED를 보고한다면 동시성 모델은 만족된 것이고, 에디터가 뒤따라오는 중일 뿐입니다1. 인덱스 유령을 쫓는 것은 이미 잘 돌아간 이전 작업에서 오후 한나절을 통째로 날리는 가장 빠른 길입니다.

FAQ

Swift 6.2에서 메인 액터를 기본값으로 두는 동시성을 어떻게 켜나요?

빌드 설정 두 개를 설정합니다. 둘 다 새 Xcode 26 프로젝트에서 기본으로 켜져 있습니다. SWIFT_DEFAULT_ACTOR_ISOLATION = MainActorSWIFT_APPROACHABLE_CONCURRENCY = YES입니다5. Swift 패키지에서는 Xcode 빌드 설정 대신 swiftSettings를 통해 동등한 upcoming-feature 플래그를 설정합니다6.

SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor는 실제로 무엇을 바꾸나요?

모듈의 코드를 기본적으로 메인 액터 격리로 만듭니다(SE-0466). 그래서 뷰와 그 모델, 헬퍼가 어노테이션 없이 모두 @MainActor가 됩니다2. 동시성은 모든 줄에서 방어하며 어노테이션을 다는 주변 조건이 아니라, 손을 뻗어 쓰는 이름 붙은 예외가 됩니다.

@concurrent는 무엇이고 언제 써야 하나요?

@concurrentasync 함수를 의도적으로, 호출자의 액터에서 벗어나 글로벌 익스큐터로 내려보냅니다4. 메인 액터 기본값 아래에서는 측정상 백그라운드 스레드가 정말로 필요한 작업(큰 페이로드 디코딩, 이미지 리사이즈)에 씁니다. @MainActor와 상호 배타적이므로, 함수는 호출자가 있는 곳에서 돌거나 의도적으로 거기서 벗어나 돌거나 둘 중 하나입니다.

왜 “Main actor-isolated … can not be referenced from a nonisolated context”가 뜨나요?

메인 액터 기본값 아래에서는 순수 함수, 싱글턴 static, 상수가 메인 액터 격리를 상속한 다음, 기본 매개변수 평가 같은 nonisolated 컨텍스트에서 호출될 때 오류를 냅니다. 수정은 억제가 아니라 그 멤버에 대한 정확한 nonisolated 어노테이션입니다. 그 값은 어떤 액터 상태도 건드리지 않으므로 공유해도 안전합니다7.

라이브러리나 프레임워크 타깃에 메인 액터 기본값을 써야 하나요?

아니요. 네트워킹 계층, 파서, 데이터 엔진은 기본값을 메인 액터로 둘 이유가 없으며, 그렇게 하면 거의 모든 것에 @concurrentnonisolated를 강요하게 됩니다. UI가 없는 타깃에서는 SWIFT_DEFAULT_ACTOR_ISOLATION을 설정하지 않은 채 두고, 옛날 방식대로 의도적으로 격리하세요.

Swift 6.2에서 nonisolated async 함수를 await하면 여전히 액터를 홉하나요?

아니요. SE-0461은 nonisolated async 함수를 글로벌 익스큐터로 홉하는 대신 자신을 호출한 액터 위에서 돌게 합니다. 그래서 await해도 더 이상 격리 경계를 넘어 끌려가거나 스코프 안의 모든 것에 Sendable을 요구받지 않습니다3.



  1. 941 패밀리 iOS 앱(Ki, Return, Get Bananas) 전반에 걸친 저자의 프로덕션 코드로, 모두 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActorSWIFT_APPROACHABLE_CONCURRENCY = YES를 켜고 출시되었습니다. 아래의 여섯 가지 오류 패턴과 수정은 Ki 1.0.0의 strict-concurrency 정리 작업 전체 세트였습니다. 

  2. Swift Evolution, SE-0466: Control default actor isolation inference. 모듈의 기본값을 @MainActor 격리로 둘 수 있게 하여, UI와 앱 타깃은 @concurrent나 명시적 액터로 옵트아웃하지 않는 한 메인 액터 위에서 돕니다. 

  3. Swift Evolution, SE-0461: Run nonisolated async functions on the caller’s actor by default. nonisolated async 함수는 기본적으로 nonisolated(nonsending)가 되어, 글로벌 익스큐터로 홉하는 대신 호출자의 액터 위에서 돕니다. 이로써 그 홉에 따라오던 경계를 넘는 Sendable 요구가 사라집니다. 

  4. Swift Evolution, SE-0461은 함수를 글로벌 익스큐터(백그라운드 스레드)에서 돌리도록 옵트인하는 @concurrent 속성을 도입합니다. @concurrentnonisolated(nonsending)는 nonisolated async 함수에 대한 서로 대립하는 두 격리 모드입니다. 함수는 호출자가 있는 곳에서 돌거나 의도적으로 거기서 벗어나 돕니다. @concurrent@MainActor나 커스텀 글로벌 액터와 결합할 수 없습니다. 

  5. SWIFT_APPROACHABLE_CONCURRENCY는 approachable-concurrency upcoming features(SE-0461 동작 포함)를 켜는 포괄 Xcode 빌드 설정이고, SWIFT_DEFAULT_ACTOR_ISOLATION은 모듈의 기본 격리를 선택합니다. 새 Xcode 26 프로젝트는 메인 액터 기본값으로 둘 다 켭니다. Donny Wals의 “Exploring concurrency changes in Swift 6.2”와 Paul Hudson의 “What’s new in Swift 6.2”에 기록되어 있으며, 둘 다 바탕이 되는 제안 SE-0461 및 SE-0466과 상호 참조됩니다. 

  6. Swift, Swift Concurrency Migration Guide, “Enabling Complete Concurrency Checking” 및 언어 모드 설정. Swift 패키지에서는 기본 격리와 approachable-concurrency 기능을 Xcode 빌드 설정이 아니라 swiftSettings의 upcoming-feature 플래그를 통해 설정합니다. 

  7. Swift, Migration Guide: global actor isolation and nonisolated. @MainActor 타입의 메서드는 메인 액터 격리를 상속하며, nonisolated는 메서드를 거기서 옵트아웃시킵니다. 이는 격리된 상태를 건드리지 않는 순수 함수에 올바른 선택입니다. 

  8. Apple Developer, MainActor.assumeIsolated(_:). 현재 실행이 이미 메인 액터 위에 있음을 단언하고, 액터 홉 없이 클로저를 동기적으로 실행합니다. 불변식이 성립하지 않으면 이 단언은 런타임에 트랩하므로, 호출자가 메인 스레드에 있다고 보장되는 곳에서만 유효합니다. 

  9. Apple, WWDC 2026 Swift Group Lab. Swift 코어 팀의 Holly와 Doug에게 귀속된 발언은 로컬에서 전사한 녹음을 풀어 옮긴 것입니다. Apple은 이 세션에 대한 공식 전사본이나 캡션을 공개하지 않았으므로, 표현은 축자적 인용이 아니라 대략적인 것입니다. 

관련 게시물

WWDC26 랩에서 Apple Swift 팀이 한 이야기

Apple의 WWDC26 Swift Group Lab은 자막 없이 공개됐습니다. 직접 로컬에서 받아쓰기해, 동시성과 ~Sendable, 로드맵에 관한 엔지니어들의 솔직한 답변을 정리했습니다.

10 분 소요

Swift의 새로운 기능 (2026): WWDC26 업데이트

WWDC26에서 발표된 Swift 6.3과 6.4: anyAppleOS 가용성, 모듈 선택자, borrow/mutate 접근자, Iterable 프로토콜, Swift Testing 상호 운용, 그리고 MLX.

13 분 소요

두 개의 MCP 서버로 Claude Code를 iOS 빌드 시스템으로 만들기

XcodeBuildMCP와 Apple의 Xcode MCP는 Claude Code에 iOS 빌드, 테스트, 디버깅에 대한 구조화된 액세스를 제공합니다. 설정, 실제 결과, 그리고 솔직한 교훈.

13 분 소요