엔지니어링 문서

클라우드 Mac에서 Kotlin Multiplatform iOS Framework 빌드하기

클라우드 Mac에서 Kotlin Multiplatform iOS Framework 빌드하기

Kotlin Multiplatform 저장소가 로컬에서 빌드된다고 해서 클라우드 Mac에서도 자연스럽게 같은 결과를 얻을 수 있는 것은 아닙니다. 가장 흔한 차이는 비즈니스 코드가 아니라 Java, Gradle Wrapper, Kotlin/Native, CocoaPods, Xcode의 조합에서 발생하며, 실기기용 산출물과 시뮬레이터용 산출물을 잘못 혼용하는 경우도 많습니다. 안정적인 접근 방식은 먼저 도구 체인을 고정한 다음 공유 모듈, Framework, 최종 Xcode workspace를 각각 검증하는 것입니다.

먼저 빌드 입력 고정하기

노드에 처음 접속한 직후 전체 파이프라인부터 실행하지 마세요. 팀 문서에 명시된 권장 버전만 확인하지 말고, 실제 빌드에 사용되는 도구를 먼저 기록해야 합니다.

sw_vers
uname -m
xcodebuild -version
xcode-select -p
java -version
./gradlew --version
bundle exec pod --version

저장소에는 gradlew, gradle/wrapper/gradle-wrapper.properties, 의존성 잠금 파일, Podfile.lock, Ruby 의존성 잠금 파일을 커밋해야 합니다. CI에서는 시스템에 우연히 설치된 전역 Gradle을 호출하지 말고 ./gradlew을 사용하세요. CocoaPods도 bundle exec pod를 통해 실행해 작업마다 서로 다른 gem 버전이 로드되는 상황을 방지해야 합니다.

Xcode를 전환해야 한다면 작업 시작 시 명시적으로 설정하고 즉시 검증하세요.

sudo xcode-select -s /Applications/Xcode.app
xcodebuild -version

도구가 “설치되어 있다”는 사실만으로는 재현 가능성을 보장할 수 없습니다. 버전, 경로, 잠금 파일, 실행 진입점을 모두 로그에서 확인할 수 있어야 재현 가능한 조건이 갖춰집니다.

실기기와 시뮬레이터 대상 분리하기

Apple Silicon Mac의 iOS 시뮬레이터도 일반적으로 arm64를 사용하지만, iPhone 실기기와 동일한 대상 플랫폼은 아닙니다. CPU 아키텍처만 확인하면 링크 단계에서 이해하기 어려운 오류가 발생할 수 있습니다.

공유 모듈에는 다음과 같이 두 대상을 명확히 유지할 수 있습니다.

kotlin {
    iosArm64()
    iosSimulatorArm64()

    targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>()
        .configureEach {
            binaries.framework {
                baseName = "SharedKit"
                isStatic = true
            }
        }
}

먼저 공유 모듈만 별도로 빌드해 문제 범위를 줄입니다.

./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 \
  --no-daemon --stacktrace

./gradlew :shared:linkReleaseFrameworkIosArm64 \
  --no-daemon --stacktrace

두 작업이 모두 성공한 뒤 CocoaPods와 Xcode 계층으로 진행하세요. 작업 이름이 존재하지 않는다면 기존 산출물을 복사해 구성 문제를 우회하지 말고 모듈 경로, 대상 선언, Framework 이름을 확인해야 합니다.

CocoaPods와 Xcode에서 잠긴 결과만 사용하기

CI에서 빌드할 때마다 Pod를 자동으로 업데이트해서는 안 됩니다. 먼저 podspec을 생성하거나 확인한 다음 Podfile.lock에 따라 엄격하게 설치하세요.

./gradlew :shared:podspec
bundle check
bundle exec pod install --project-directory=iosApp

그다음 프로젝트 파일이 아니라 workspace를 빌드해야 합니다.

xcodebuild \
  -workspace iosApp/App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/.build/DerivedData" \
  CODE_SIGNING_ALLOWED=NO \
  build

CODE_SIGNING_ALLOWED=NO는 시뮬레이터 빌드 검증에는 적합하지만, 실기기 아카이브에 필요한 서명 구성을 대체할 수는 없습니다. 프로젝트에서 사용자 정의 구성을 사용한다면 해당 .xcconfig의 Framework 검색 경로가 로컬 절대 경로로 지정되어 있지 않은지도 확인해야 합니다.

계층별 검사로 문제 범위 좁히기

검사 계층 권장 명령 실패 시 우선 확인 항목
Kotlin 소스 코드 ./gradlew :shared:compileKotlinIosSimulatorArm64 expect/actual, 의존성 가시성
Framework 링크 linkDebugFrameworkIosSimulatorArm64 대상 플랫폼, 네이티브 의존성
Pods 통합 bundle exec pod install 잠금 파일, Ruby 환경, podspec
Xcode 빌드 xcodebuild -workspace Scheme, 검색 경로, 빌드 구성

다운로드는 캐시하되 쓰기 가능한 작업 공간은 공유하지 않기

~/.gradle/caches~/.konan은 다시 다운로드하는 비용이 크므로 일반적으로 캐시할 가치가 있습니다. 반면 Pods와 DerivedData는 더 신중하게 다뤄야 합니다. 병렬 작업이 쓰기 가능한 DerivedData를 공유하면 한 작업이 다른 브랜치의 중간 파일을 읽을 수 있습니다. 가장 안전한 방법은 작업별로 독립된 디렉터리를 만들고 작업이 끝난 뒤 삭제하는 것입니다.

캐시 키에는 최소한 다음 항목이 포함되어야 합니다.

  • gradle-wrapper.properties와 의존성 잠금 파일의 해시
  • libs.versions.toml, Podfile.lock, Ruby 의존성 잠금 파일의 해시
  • xcodebuild -version 출력
  • Java 주 버전과 Kotlin 플러그인 버전
  • 대상 구성이 Debug인지 Release인지 여부

사용자 디렉터리 전체를 하나의 장기 캐시로 묶지 마세요. 캐시 무효화가 어려워질 뿐 아니라 자격 증명, 로그, 관련 없는 상태까지 섞일 수 있습니다. 캐시를 복원한 후에는 경량 컴파일을 한 번 실행해야 합니다. 실패하면 노드의 모든 개발 디렉터리를 곧바로 삭제하지 말고 현재 작업의 캐시부터 정리하세요.

오류 형태에 따라 원인을 찾고 증거 남기기

building for iOS Simulator, but linking in object file built for iOS가 발생하면 CocoaPods를 다시 설치하기보다 먼저 Framework가 어느 대상에서 생성되었는지 확인하세요. framework not found가 발생하면 Build Settings에서 확장된 검색 경로, Framework의 실제 위치, 스크립트 단계가 컴파일 전에 실행되는지를 확인해야 합니다.

주요 진단 출력을 작업 첨부 파일로 저장할 수 있습니다.

xcodebuild -showBuildSettings \
  -workspace iosApp/App.xcworkspace \
  -scheme App > .build/build-settings.txt

find shared -type d -name '*.framework' -print \
  > .build/framework-inventory.txt

로컬에서는 성공하지만 원격에서 실패한다면 xcodebuild -version, java -version, 잠금 파일 해시, 환경 변수, workspace 생성 시각을 비교하세요. 처음부터 모든 캐시를 비우면 안 됩니다. 현장 정보가 사라질 뿐 아니라 문제가 일시적으로 없어져도 원인을 설명할 수 없게 됩니다.

병합 전 최소 검사 항목

병합 요청에서는 최소한 공유 모듈의 시뮬레이터 빌드, Xcode workspace 빌드, Release 실기기용 Framework 링크를 검증해야 합니다. 병렬 작업에는 독립된 DerivedData를 사용하고, 로그에는 도구 버전과 실패한 전체 명령을 남기며, 빌드 산출물은 플랫폼별로 이름을 지정하세요. 이렇게 하면 Kotlin 계층, Pods 계층, Xcode 계층의 책임 경계가 충분히 명확해져 대부분의 장애를 한 번의 파이프라인 실행 안에서 진단할 수 있습니다.

자주 묻는 질문

arm64 실기기 Framework를 Apple Silicon 시뮬레이터에서 사용할 수 있나요?

사용할 수 없습니다. 명령어 아키텍처가 같아 보여도 플랫폼 식별자가 다르므로 iosArm64와 iosSimulatorArm64 타깃을 각각 빌드해야 합니다.

CI에서 어떤 Kotlin Multiplatform 캐시를 보관해야 하나요?

Gradle 다운로드 캐시와 Kotlin/Native 도구 체인을 우선 보관하고, 잠금 파일과 Gradle Wrapper, Xcode 및 Java 버전을 캐시 키에 포함하는 것이 안전합니다.

전용 물리 노드

빌드 작업을 전용 클라우드 Mac에서 실행하세요

VMOrbit M4 또는 VMOrbit M4 Pro를 선택하고 팀의 위치에 따라 노드를 지정하세요. 실제 사용 가능 여부와 제공 정보는 콘솔에서 실시간으로 확인되는 내용을 기준으로 합니다.

클라우드 Mac 선택