- 컴파일러 버전을 올리는 것과 언어 모드를 올리는 것의 차이는?
- 마이그레이션 중 만나는 대표적 경고(글로벌 가변 변수, non-Sendable 캡처 등)의 해결 패턴은?
답변 ① — 서로 다른 세 개의 스위치 🔥 핵심
흔히 두 개로 뭉뚱그리지만, 실제로 관련된 스위치는 세 개이고 서로 독립입니다.
- 컴파일러/툴체인 버전 — 어떤 Swift 컴파일러 바이너리를 쓰느냐. 새 문법과 진단 능력을 가져옵니다. 그 자체로는 기존 코드를 깨지 않습니다.
- 언어 모드(
SWIFT_VERSION) — Swift 5 vs 6. 타깃 단위로 설정합니다. - strict concurrency 체크 레벨(
minimal/targeted/complete) — 데이터 레이스를 얼마나 검사할지의 직교(orthogonal) 노브입니다.
이 셋을 조합하면 결과는 이렇게 갈립니다.
- Swift 6 언어 모드 = 데이터 레이스 검사가 에러(complete 검사를 함의). 빌드가 막힙니다.
- Swift 5 언어 모드 + strict =
targeted/complete= 같은 검사가 '경고'로만 뜹니다. 빌드는 계속됩니다. - Swift 5 기본(
minimal) = 동시성 진단이 거의 나오지 않습니다(명시적으로@Sendable·@MainActor를 쓴 곳만).
그래서 "Swift 6 컴파일러만 깔면 경고가 뜬다"는 strict 레벨을 올렸을 때만 맞는 말이고, 컴파일러 버전을 올리는 것과 언어 모드를 올리는 것은 전혀 다른 스위치입니다. 정석 순서는 언어 모드를 5로 둔 채 strict를 minimal → targeted → complete로 올리며 경고를 다 잡고, 마지막에 타깃별로 언어 모드를 6으로 전환하는 것입니다.
// swift-tools-version: 6.0 ← ① 툴체인/매니페스트 버전
.target(
name: "MyApp",
swiftSettings: [
.swiftLanguageMode(.v5), // ② 언어 모드 = 5 → 진단은 '경고'
.enableExperimentalFeature("StrictConcurrency=complete") // ③ strict 레벨 = complete
]
)
// ②를 .v6 로 올리는 순간, 같은 진단이 '에러'가 되어 빌드가 막힌다.
// 그래서 ③으로 경고를 먼저 다 잡은 뒤 ②를 올리는 것이 정석.답변 ② — 격리하거나, 스냅샷을 뜨거나, 임시로 막는다
대표 경고별 해결 패턴은 다음과 같습니다.
- 글로벌 가변 변수(
static var) — 우선순위대로: ①let+ Sendable로 불변화, ②@MainActor나 actor로 격리, ③ 최후의 수단으로nonisolated(unsafe)+private(set). ③은 안전을 직접 책임지는 것이므로 사유를 남깁니다. - non-Sendable 캡처(
Task·@Sendable클로저가 가변 변수를 캡처) — 캡처 리스트로 불변 스냅샷[value]를 뜨거나, 값 타입으로 바꾸거나, 상태 자체를 actor로 옮깁니다. - 서드파티 non-Sendable 타입 —
@preconcurrency import로 임시 억제합니다. 반드시 사유 주석과 제거 티켓을 함께 남깁니다.
경고를 없애려고 @MainActor를 아무 데나 붙이는 것은 안티패턴입니다. 무거운 작업까지 메인에서 돌게 되어 성능이 나빠집니다. 다만 app 타깃이라면 Swift 6.2의 Default Actor Isolation = MainActor가 경고를 가장 크게 줄여 주는 정당한 레버입니다(대부분의 앱 코드는 실제로 메인에서 도니까요).
// ① 글로벌 가변 변수
enum Config { nonisolated(unsafe) static var shared: Config? } // 최후의 수단
// → 더 나은 방법: let + Sendable, 또는 @MainActor / actor 로 격리
// ② non-Sendable 캡처 — 캡처 리스트로 불변 스냅샷을 뜬다
var query = "swift"
Task { [query] in
await search(query) // 가변 변수가 아니라 복사된 값을 캡처
}
// ③ 서드파티 non-Sendable
@preconcurrency import OldLibrary // 임시 조치 + 사유 주석 + 제거 티켓쉽게 이해하기
(질문 ①) 스위치가 세 개라고 생각하면 쉬워요. 학교 시험에 비유해 볼게요.
- 컴파일러 버전 = 검사 도구를 최신으로 바꾼 것. 더 많은 걸 볼 수 있게 됐을 뿐, 그 자체로 벌점을 주진 않아요.
- strict 레벨 = 얼마나 깐깐하게 볼지. 올릴수록 잔소리(경고)가 많아집니다.
- 언어 모드 6 = "통과 못 하면 못 나감"으로 규칙을 바꾼 것. 잔소리가 진짜 벌점(에러)이 돼요.
그러니까 도구만 바꾸면 잔소리도 별로 안 나오고, 깐깐함을 올리면 잔소리가 많아지고, 규칙까지 바꾸면 통과가 필수가 됩니다. 세 개를 한꺼번에 다 세게 하면 벌점 폭탄을 맞아서 아무것도 못 해요. 요령은 깐깐함을 조금씩 올리며 잔소리를 하나씩 다 고치고, 맨 마지막에 규칙을 강하게 바꾸는 겁니다.
(질문 ②) 잔소리를 고치는 방법도 세 가지예요. 아무나 못 바꾸게 못 박거나(불변으로 만들기), "이건 이 방에서만 만져"라고 방을 정해주거나(actor·MainActor로 격리), 그것도 안 되면 사진을 찍어서 그 사본만 들고 가는 거예요(캡처 스냅샷). 남의 라이브러리라 내가 못 고칠 때는 "일단 이건 넘어가자"라고 표시해 두되, 반드시 나중에 치울 메모를 남깁니다.
도구 교체 ≠ 규칙 강화. 경고로 먼저 다 잡고, 에러는 맨 마지막에 켠다.