엔지니어링 가이드

iOS와 Mac Catalyst 이중 빌드 게이트 구축하기

iOS와 Mac Catalyst 이중 빌드 게이트 구축하기

원래 iPhone과 iPad만 지원하던 프로젝트에서 Mac Catalyst를 활성화하면 “iOS에서 컴파일되니 Mac에서도 문제없이 빌드될 것”이라고 판단하기 쉽습니다. 하지만 클라우드 Mac의 자동화 작업에서는 주로 의존성의 플랫폼 선언, 리소스 소속, 조건부 컴파일, 캐시 혼용 때문에 문제가 발생합니다. 기존 명령에 destination 하나를 추가하는 방식으로는 충분하지 않습니다. 두 플랫폼을 소스 코드는 공유하지만 각각 독립적으로 검증해야 하는 별도의 빌드 제품으로 다뤄야 합니다.

프로젝트가 실제로 두 대상을 지원하는지 먼저 확인하기

먼저 Xcode의 대상 설정에서 Mac Catalyst를 활성화한 다음, 해당 변경 사항이 프로젝트 파일에 커밋되었는지 확인합니다. 그래픽 인터페이스의 체크 상태만 확인해서는 안 됩니다. 자동화 작업에서는 빌드 설정으로부터 최종 값을 읽어야 합니다.

xcodebuild \
  -project DemoApp.xcodeproj \
  -scheme DemoApp \
  -showBuildSettings |
grep -E 'SUPPORTS_MACCATALYST|PRODUCT_BUNDLE_IDENTIFIER|SDKROOT'

SUPPORTS_MACCATALYST 값은 YES여야 합니다. 프로젝트가 작업 공간을 사용한다면 -project-workspace로 바꿉니다. 이어서 xcodebuild -showdestinations를 실행해 공유 scheme이 iOS Simulator와 Mac Catalyst destination을 모두 노출하는지 확인합니다. 명령줄에서 scheme을 찾지 못한다면 destination 문자열을 계속 수정하기 전에 scheme이 Shared로 설정되어 있는지 먼저 확인합니다.

Swift Package, 바이너리 의존성, 내부 모듈이 지원하는 플랫폼 범위도 하나씩 검토해야 합니다. 어떤 패키지가 iOS에서 컴파일된다고 해서 Mac Catalyst 지원까지 선언한 것은 아닙니다. 비공개 소스 바이너리 의존성은 XCFramework에 Catalyst용 슬라이스가 포함되어 있는지 확인해야 합니다. 슬라이스가 없으면 링크 단계에 이르러서야 실패합니다.

캐시와 결과 디렉터리 격리하기

iOS와 Catalyst는 서로 다른 모듈 캐시, 중간 객체, 링크 산출물을 생성합니다. 지속적 통합 작업이 하나의 DerivedData를 공유하면 작업 순서에 따라 실패 여부가 달라지고, 정리 후에는 문제가 일시적으로 사라질 수 있습니다. 플랫폼별로 고정 디렉터리를 지정하고 결과 번들도 함께 보관하는 것이 좋습니다.

set -euo pipefail

xcodebuild \
  -workspace DemoApp.xcworkspace \
  -scheme DemoApp \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath build/derived-ios \
  -resultBundlePath build/results-ios.xcresult \
  CODE_SIGNING_ALLOWED=NO \
  build-for-testing

xcodebuild \
  -workspace DemoApp.xcworkspace \
  -scheme DemoApp \
  -destination 'platform=macOS,variant=Mac Catalyst,arch=arm64' \
  -derivedDataPath build/derived-catalyst \
  -resultBundlePath build/results-catalyst.xcresult \
  CODE_SIGNING_ALLOWED=NO \
  build-for-testing

여기서 서명을 먼저 비활성화하는 이유는 게이트 검증을 소스 코드, 의존성, 링크의 유효성에 집중시키기 위해서입니다. 아카이브와 배포 서명은 후속 작업으로 분리해 플랫폼 호환성 오류와 서명 설정 오류가 하나의 로그에 섞이지 않도록 해야 합니다.

~/Library/Developer/Xcode/DerivedData 전체 삭제를 기본 해결책으로 삼지 마세요. 현재 작업의 디렉터리만 정확히 삭제하면 같은 시스템에서 실행 중인 다른 작업에 영향을 주지 않으면서 실패 당시의 상태도 보존할 수 있습니다.

플랫폼 차이를 흩어진 분기 대신 한곳에 모으기

Catalyst는 UI 수명 주기, 메뉴, 창 동작, 일부 시스템 기능이 iOS와 다릅니다. 조건부 컴파일은 어댑테이션 계층에 집중시키고 targetEnvironment(macCatalyst)가 비즈니스 코드 곳곳에 흩어지지 않도록 해야 합니다.

enum PlatformLayout {
    static var usesDesktopNavigation: Bool {
        #if targetEnvironment(macCatalyst)
        return true
        #else
        return false
        #endif
    }
}

플랫폼 전용 API에는 가용성 검사도 함께 적용해야 합니다. 조건부 컴파일은 현재 컴파일 대상만 나타낼 뿐, 실행 중인 시스템 버전에서 해당 API를 반드시 제공한다는 의미는 아닙니다. 공통 프로토콜에 안정적인 인터페이스를 먼저 정의한 뒤 iOS와 Catalyst에서 각각 구현하면 단위 테스트로 양쪽 동작을 직접 검증할 수 있습니다.

리소스도 별도로 확인해야 합니다. 글꼴, 개인정보 보호 설명 파일, 현지화 리소스, 데이터 모델은 Target Membership 선택이 누락되어 한쪽 제품에만 포함될 수 있습니다. 실행 후 빈 화면을 발견할 때까지 기다리지 말고, 빌드가 끝난 뒤 산출물 디렉터리에서 핵심 파일의 존재 여부를 확인합니다.

확인 항목 iOS Mac Catalyst
의존성 링크 가능 여부 시뮬레이터 슬라이스 사용 가능 Catalyst 슬라이스 사용 가능
리소스 소속 App 번들에서 확인 가능 Catalyst App 번들에서 확인 가능
조건부 컴파일 모바일 분기 데스크톱 어댑테이션 분기
결과 증거 독립 xcresult 독립 xcresult

실패를 추적 가능한 단계로 분리하기

이중 대상 작업을 하나의 포괄적인 “빌드 실패”로 처리해서는 안 됩니다. 의존성 해석, 서명 없는 컴파일, 단위 테스트, 아카이브, 배포 검증 단계로 순서대로 나누는 것이 좋습니다. 앞 단계가 통과한 뒤에만 다음 단계로 진행하고, 로그도 플랫폼별로 이름을 지정합니다.

컴파일 실패 시 먼저 확인할 네 가지 신호

No such module이 발생하면 먼저 의존성의 플랫폼 선언과 빌드 대상을 확인합니다. 아키텍처가 일치하지 않으면 바이너리 슬라이스를 점검해야 하며, 전역 제외 아키텍처를 바로 추가해서는 안 됩니다. unavailable API 오류가 발생하면 어댑테이션 계층으로 돌아가 조건부 컴파일과 가용성 검사를 보완합니다. 리소스가 중복되거나 누락되었다면 Copy Bundle Resources와 Target Membership을 확인합니다.

빌드 스크립트에도 특정 플랫폼을 전제로 한 가정이 포함될 수 있습니다. 예를 들어 스크립트에서 SDK를 iphoneos로 고정하면 Catalyst 작업이 잘못된 경로를 읽게 됩니다. 스크립트는 Xcode가 주입하는 PLATFORM_NAME, SDKROOT, TARGET_BUILD_DIR을 사용해야 하며, 알 수 없는 플랫폼에서는 즉시 종료해야 합니다.

병합 전 체크리스트 마련하기

최소한의 게이트 검증에서는 두 대상 모두 깨끗한 디렉터리에서 의존성 해석과 서명 없는 컴파일을 완료하고, 각각의 .xcresult를 저장할 수 있어야 합니다. 핵심 모듈의 단위 테스트는 플랫폼 어댑테이션 인터페이스를 포함해야 합니다. 창, 메뉴, 드래그 앤 드롭과 관련된 동작에는 Catalyst 전용 테스트를 추가로 배치합니다.

커밋하기 전에 다음 순서로 점검할 수 있습니다.

  • scheme이 공유되어 있으며 명령줄에서 두 destination을 모두 나열할 수 있다.
  • iOS와 Catalyst가 독립된 DerivedData를 사용한다.
  • 의존성이 두 플랫폼을 명시적으로 지원하며 바이너리 슬라이스가 완전하다.
  • 플랫폼 판별 로직이 어댑테이션 계층에 집중되어 있고 공통 로직이 중복 구현되지 않았다.
  • 핵심 리소스가 두 제품에 모두 포함된다.
  • 실패 로그, 결과 번들, 빌드 설정 스냅샷이 플랫폼별로 보관된다.
  • 컴파일 게이트와 아카이브 서명 작업이 분리되어 실행된다.

G-Mini 또는 다른 원격 빌드 환경에 처음 적용할 때는 두 번 연속 실행한 다음, 두 대상의 실행 순서를 바꿔 다시 실행합니다. 결과가 실행 순서에 따라 달라진다면 공유 캐시, 스크립트 임시 디렉터리, 정리되지 않은 생성 파일을 우선 점검해야 합니다. 안정적인 이중 대상 파이프라인의 기준은 가끔 두 빌드가 동시에 통과하는 것이 아니라, 깨끗한 작업 공간과 어떤 실행 순서에서도 일관된 결과를 얻는 것입니다.

자주 묻는 질문

iOS와 Mac Catalyst가 DerivedData를 공유해도 되나요?

짧은 로컬 확인에서는 가능하지만 CI에서는 분리하는 편이 안전합니다. 중간 산출물의 교차 오염을 막고 실패한 플랫폼만 정리할 수 있습니다.

처음부터 전체 테스트를 실행해야 하나요?

아닙니다. 먼저 두 대상의 서명 없는 컴파일과 핵심 단위 테스트를 통과시킨 뒤 UI 테스트, 아카이브, 서명 검증을 단계적으로 추가합니다.

Catalyst 빌드 실패 시 무엇부터 확인해야 하나요?

대상의 Catalyst 지원 여부, 의존성의 플랫폼 지원, 조건부 컴파일 분기, 리소스의 Target Membership과 플랫폼별 Build Phase를 확인합니다.

전용 물리 노드

다음 빌드 큐를 위한 클라우드 Mac

두 가지 M4 구성과 5개 노드를 비교하고, 실제 작업 주기에 맞춰 일간·주간·월간·분기 임대를 선택하세요.

클라우드 Mac 요금제 선택