전용 원격 Mac에서 Swift 6 마이그레이션:엄격 동시성 CI 게이트와 Actor 격리 Runbook

Swift 6은 동시성 정확성을 「런타임 크래시」에서 컴파일 시점 오류로 옮깁니다. -strict-concurrency=complete를 켜면 데이터 경합과 Actor 경계 위반이 빌드를 실패시킵니다. 본문은 NUKCLOUD 전용 원격 Mac 노드에서 이 플래그를 CI 게이트에 넣는 방법과, PR 병합 전 동시성 준수를 통과하게 하는 6단계 점진 마이그레이션 체크리스트를 설명합니다.

Swift 6 언어 모드는 컴파일러를 「동시성 감사관」으로 만듭니다. @MainActor 오용이나 Actor 간 공유를 코드 리뷰로 찾지 않고, xcodebuild 단계에서 바로 오류를 냅니다. 전체 컴파일을 돌리면서 이웃 CPU 경합으로 꼬리 지연이 흔들리지 않는 안정적이고 버전 고정 가능한 빌드 노드가 필요합니다. 본문은 NUKCLOUD 전용 원격 Mac 노드에서 Swift 6 엄격 동시성 검사를 단계적으로 도입하고 PR 병합 게이트에 고정하는 방법을 보여 줍니다.

00지금 마이그레이션하는 이유 — 그리고 전용 노드가 필요한 이유

Swift 6 엄격 동시성 검사(-strict-concurrency=complete)로 나오는 오류 수는 코드베이스 규모에 비례합니다. 중간 규모 iOS 프로젝트에서 최초 활성화 시 200–800개의 컴파일 오류는 흔합니다. 수정 기간은 수 주에서 수 개월에 이르며, 그동안 CI 환경은 다음을 보장해야 합니다:

  • Xcode 마이너 버전 고정: Actor isolation 진단 문구는 Xcode 버전마다 달라지므로, 버전을 고정해야 CI 출력을 비교할 수 있습니다.
  • 이웃 CPU 경합 없음: 엄격 동시성 전체 컴파일은 일반 debug 빌드보다 20–40% 더 오래 걸리며, 전용 노드는 P95 꼬리 지연을 안정적으로 유지합니다.
  • DerivedData 네임스페이스 격리: 여러 PR이 동시에 동시성 검사를 돌릴 때는 각각 독립된 증분 컴파일 캐시가 필요하며, 그렇지 않으면 결과를 신뢰할 수 없습니다.
팁: CI가 「공유 분 단위」 호스팅 macOS 풀을 쓰고 있다면, Swift 6 게이트 전에 대기열 P95를 평가하세요. 노드 의미는 NUKCLOUD 전용 Apple Silicon 노드 Runbook을 참고하세요.

01세 가지 동시성 검사 수준과 마이그레이션 경로

Swift는 OTHER_SWIFT_FLAGS(Xcode Build Settings) 또는 Package.swiftswiftSettings에서 설정할 수 있는 컴파일러 플래그 세 단계를 제공합니다:

플래그의미권장 단계
-strict-concurrency=minimalasync/await 기본 문법만 검사하고 Actor 경계는 검사하지 않음기존 코드 워밍업(1–2주차)
-strict-concurrency=targetedSendable, @MainActor 등으로 명시 표기된 타입의 Actor 경계 검사모듈별 수정(3–6주차)
-strict-concurrency=complete전량 검사, Swift 6 언어 모드와 동등. 명시적으로 처리하지 않은 동시 공유는 모두 오류오류 0건 달성 후 게이트에 포함

실무에서는 모듈별로 점진하는 것을 권장합니다. 의존이 없거나 적은 리프 모듈부터 complete를 켜고, 핵심 모듈로 확장하세요. SwiftPM의 .enableUpcomingFeature("StrictConcurrency")로 target별 수준을 정밀 제어할 수 있습니다.

02Actor 격리 경계: 세 가지 빈번한 오류

마이그레이션 기간 컴파일 오류의 약 90%는 아래 세 가지에 몰립니다. 이 세 가지를 먼저 처리하면 대부분의 빨간 빌드를 없앨 수 있습니다:

① Actor 간 비-Sendable 타입 접근

Swift — 전형적 오류 예시
// ❌ 오류: @MainActor 밖에서 @MainActor 격리 상태 접근
actor DataFetcher {
    func load() async {
        let result = await networkClient.fetch()
        // ❌ passing argument of non-sendable type 'UserModel' across actor boundary
        await MainActor.run { viewModel.update(result) }
    }
}

수정: UserModelSendable을 구현하게 하거나(값 타입은 보통 자동 충족), 경계를 넘는 변환을 Actor 경계 안에서 끝냅니다.

② @MainActor 누락

UIKit/SwiftUI의 ViewController, ObservableObject 하위 클래스는 마이그레이션 전 암묵적으로 메인 스레드에서 실행되었습니다. Swift 6은 명시적 @MainActor 표기를 요구합니다. Xcode 「Fix」로 일괄 수정할 수 있으나, UI가 아닌 로직까지 끌어오지 않도록 각 위치를 검토하세요.

③ 전역 변수 동시 접근

Swift — 전역 변수 수정 예시
// ❌ Swift 6에서는 전역이 Sendable 또는 nonisolated 여야 함
var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ 마이그레이션 중 nonisolated(unsafe) 명시
nonisolated(unsafe) var sharedCache: NSCache<NSString, AnyObject> = .init()

// ✅ 권장: Actor로 감싸고 async API 제공
actor CacheStore {
    private let cache = NSCache<NSString, AnyObject>()
    func object(forKey key: String) -> AnyObject? { cache.object(forKey: key as NSString) }
}

03원격 Mac에서 CI 게이트 구성

PR 게이트에 엄격 동시성 검사를 넣는 핵심은 일반 테스트 Job과 별도 Job으로 동시성 검사를 실행하는 것입니다. NUKCLOUD에서는 가격 페이지에서 디스크·리전을 고른 뒤 주문 페이지로 SSH·Runner 기준선을 받으세요.

GitHub Actions — .github/workflows/swift6-concurrency-check.yml
name: Swift 6 Concurrency Gate

on:
  pull_request:
    branches: [main, release/*]

jobs:
  concurrency-check:
    runs-on: self-hosted          # NUKCLOUD 전용 원격 Mac Runner
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - name: Xcode 선택(마이너 버전 고정)
        run: sudo xcode-select -s /Applications/Xcode_16.3.app

      - name: Restore DerivedData cache
        uses: actions/cache@v4
        with:
          path: ~/DerivedData/swift6-check
          key: swift6-${{ runner.name }}-${{ hashFiles('**/*.xcodeproj/project.pbxproj', '**/Package.resolved') }}
          restore-keys: swift6-${{ runner.name }}-

      - name: Build with strict concurrency (complete)
        run: |
          xcodebuild build \
            -project MyApp.xcodeproj \
            -scheme MyApp \
            -destination 'generic/platform=iOS Simulator' \
            -derivedDataPath ~/DerivedData/swift6-check \
            OTHER_SWIFT_FLAGS="-strict-concurrency=complete" \
            | xcbeautify --renderer github-actions

      - name: Upload build log on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: swift6-build-log-${{ github.sha }}
          path: ~/DerivedData/swift6-check/Logs/Build/
DerivedData 버킷 규칙: -derivedDataPath ~/DerivedData/swift6-check를 일반 빌드 DerivedData와 분리해 동시성 검사 산출물이 디버그 캐시를 오염시키지 않게 합니다. 여러 PR이 동시에 돌 때는 runner.name이나 PR 번호로 추가 격리하세요.

046단계 점진 마이그레이션 Runbook

  1. 01
    기준선 스냅샷: 전용 원격 Mac에서 -strict-concurrency=minimal로 전체 컴파일을 한 번 돌리고, 오류 수와 컴파일 시간을 기록해 정량화된 출발점을 만듭니다.
  2. 02
    리프 모듈 우선: SwiftPM 의존 그래프에서 하위 의존이 없는 target부터 complete를 켭니다. target마다 수정이 끝나면 별도 PR로 올려 diff를 검토 가능하게 유지합니다.
  3. 03
    CI 브랜치 전략: Git의 feature/swift6-migration 장기 브랜치에서 동시성 검사 Job을 warn-only(continue-on-error: true)로 돌려 일상 PR 병합을 막지 않습니다. 모듈이 0 오류에 도달했을 때만 해당 모듈 검사를 게이트(block-merge)로 승격합니다.
  4. 04
    Xcode 버전 고정: xcode-select 버전을 Runbook과 README에 적어 로컬 Mac 개발자와 CI 노드가 같은 마이너 버전을 쓰게 하고, 「로컬은 통과, CI는 실패」 잡음을 줄입니다.
  5. 05
    오류 추세 대시보드: CI 실행마다 Swift 6 오류 수를 구조화된 로그나 Build Metrics에 기록하고, 꺾은선 그래프로 진행을 추적하세요. 팀에 「이번 주 X개 감소」 같은 주간 목표를 둡니다.
  6. 06
    전량 게이트 인수: 모든 모듈이 0 오류에 도달하면 main 브랜치 Branch Protection에서 「Require swift6-concurrency-check to pass」를 켜고, 모든 target Build Settings에 SWIFT_VERSION = 6을 넣어 Swift 6 언어 모드로 전환합니다.

05로컬 Mac과 전용 원격 노드 대조

Swift 6 마이그레이션은 개발자 로컬의 빠른 시행착오CI 전량의 신뢰할 수 있는 검증이 모두 필요하며, 노드 요구가 다릅니다:

차원개발자 로컬 MacNUKCLOUD 전용 원격 Mac(CI)
Xcode 버전개발자가 직접 관리, 차이 발생 가능스크립트로 마이너 버전 고정, 팀 전체 일치
DerivedData일상 개발과 공유, 증분 캐시 오염 가능별도 경로, PR 또는 Runner별 버킷
CPU 가용성다른 앱과 경합, 컴파일 시간 분산베어메탈 전용, P95 지연 예측 가능
결과 신뢰도
로컬 통과가 CI 통과를 의미하지 않음게이트 단계의 최종 판정 환경
적합한 용도단일 모듈 빠른 시도와 Xcode 진단 읽기전량 동시성 검사 게이트와 일일 회귀

06자주 묻는 질문

Swift 6 엄격 동시성 검사와 「Swift 6 언어 모드」는 같은 것인가요?
완전히 같지는 않습니다. -strict-concurrency=complete는 컴파일러 플래그로, Swift 5 언어 모드에서도 켤 수 있어 모든 오류를 본 뒤 전환 시점을 결정할 수 있습니다. 정식 Swift 6 언어 모드(SWIFT_VERSION = 6)는 여기에 더해 일부 구문 설탕을 제거합니다. complete 플래그로 오류를 먼저 없앤 뒤 언어 모드를 전환하는 것을 권장합니다.
서드파티 의존성(CocoaPods / SPM)이 아직 Swift 6을 지원하지 않으면?
Package.swift에서 .unsafeFlags(["-strict-concurrency=minimal"])로 특정 의존성의 검사 수준을 낮추거나, Podfile post_install 훅에 SWIFT_STRICT_CONCURRENCY = minimal을 설정할 수 있습니다. 2026년에는 대부분 주요 라이브러리가 마이그레이션을 마쳤으니 Release Notes를 먼저 확인하세요.
NUKCLOUD 노드에서 여러 PR의 동시성 검사를 동시에 돌릴 수 있나요?
가능합니다. Runner 등록 시 --concurrent-jobs N을 설정하고, DerivedData를 PR 번호로 버킷(-derivedDataPath ~/DerivedData/pr-${{ github.event.number }})하면 Job이 서로 독립적으로 컴파일합니다. 전용 베어메탈 노드의 핵심 이점은 이웃 CPU 경합이 없고 동시 Job 완료 시간 분산이 작다는 점입니다.
요금과 노드 사양은 어디서 보나요?
분 단위 풀·자체 Mac·전용 원격 노드 중 무엇을 쓸까요?
수 주간 -strict-concurrency=complete 게이트를 고정할 때는 NUKCLOUD 멀티리전 베어메탈 Mac / 클라우드 Mac 노드가 안정적이며, 콘솔 Runbook 6단계 인수와 맞추기 쉽습니다.