Email me

2026년 10월 24일 자 영어 원문을 번역했습니다. 숫자와 출처는 원문과 같습니다. 영어 원문 읽기 →

개발 · · 10분 분량 · Matt Busel

분류: iOS와 Swift 앱 개발 가이드

Windows에서 iOS 앱 만들기: Mac 없이 앱 21개를 출시한 방법

Windows에서 iOS 앱 만들기: Mac 없이 앱 21개를 출시한 방법

요약: Mac이 필요한 부분을 분 단위로 빌리면 Windows에서도 iOS 앱을 만들 수 있습니다. 이 방법으로 iPhone 앱 21개가 책상 위에 Mac 하나 없이 Windows PC에서 App Store Connect까지 갔습니다. 첫 앱은 CI 실행 13번이 실패했습니다. 마지막 10개는 0번이었습니다. 전체 파이프라인, 코드, 그리고 실행 한 번씩을 날리게 만든 함정을 전부 정리했습니다.

검색하신 내용이니 짧은 답부터 드리겠습니다. 네, Windows에서 iPhone 앱을 빌드하고 출시할 수 있습니다. Swift 코드는 PC에서 작성하고, 컴파일과 서명과 업로드는 직접 만질 일 없는 macOS 머신이 합니다. 그 머신은 GitHub Actions macOS 러너이고, 공개 저장소라면 GitHub는 요금을 받지 않습니다.

솔직한 단서가 있습니다. Xcode는 macOS에서만 돌기 때문에 Apple은 여전히 어딘가에 Mac이 필요합니다. 유료 Apple Developer Program 멤버십(연 $99)도 필요합니다. 그 밖의 모든 것은, 대부분의 가이드가 키체인 접근이 필요하다고 말하는 서명 인증서까지 포함해서, App Store Connect API를 통해 Windows 터미널에서 처리할 수 있습니다.

2026년 9월 10일부터 10월 8일까지 이 파이프라인은 앱 저장소 21개에서 App Store 빌드 139번을 실행했습니다. 21개 앱 모두 CI 로그에 업로드 기록이 있고, 10월 16일 기준 8개가 미국 App Store에 출시되어 있습니다. 어떻게 동작하는지, 그리고 어디서 깨졌는지 설명하겠습니다.

파이프라인 애니메이션 다이어그램: Windows PC가 gh workflow run을 실행하고, GitHub macOS 러너가 Xcode 26을 골라 52초 만에 컴파일하고, 1초 만에 서명 인증서를 가져온 뒤, fastlane이 2분 31초 만에 빌드와 업로드를 마칩니다. 총 4분 22초.
2026년 10월 8일, Chain 1.2가 Windows의 명령 하나로 App Store Connect까지 간 과정. 단계별 시간은 GitHub Actions 실행 로그 기준.

시작하기 전에 필요한 것

  • git, Python, GitHub CLI가 설치된 Windows PC. 에디터는 아무거나 괜찮습니다. 어차피 Xcode를 클릭할 수 없으니 코딩 에이전트가 있으면 큰 도움이 됩니다.
  • 유료 Apple Developer 계정.
  • GitHub 저장소. 공개 저장소면 macOS 사용 시간이 무료입니다. 비공개도 되지만 비용이 듭니다(숫자는 아래에).

사람만 할 수 있는 일이 두 가지 있고, 각각 한 번씩만 하면 됩니다.

  1. App Store Connect API 키 만들기(Users and Access, Integrations). .p8 파일, 키 ID, 발급자(issuer) ID를 받습니다. 이 값들은 GitHub secrets에만 넣고 다른 곳에는 두지 않습니다.
  2. App Store Connect에서 앱 레코드 만들기. API로 앱을 읽고 수정할 수는 있지만, 저희가 보낸 POST /v1/apps는 403을 돌려받았기 때문에 이것만은 웹 UI에서 클릭해야 합니다. 나머지 상품 정보 필드(이름, 부제, 설명, 키워드, 스크린샷)는 전부 스크립트로 처리합니다.

1단계: .xcodeproj는 절대 커밋하지 않기

Xcode 프로젝트 파일(.pbxproj)은 사람이 직접 고치기 어려운 거대한 자동 생성 덩어리이고, Windows에서는 Xcode를 열어 대신 고치게 할 수도 없습니다. 그러니 아예 두지 마세요. XcodeGen이 매 CI 실행 시작 시 짧은 YAML 파일로 프로젝트를 만들어 줍니다. 아래는 모든 앱의 출발점이 된 실제 템플릿(templates/project.yml, 일부 생략)입니다.

name: MyApp
options:
  bundleIdPrefix: com.example
  deploymentTarget:
    iOS: "17.0"
settings:
  base:
    DEVELOPMENT_TEAM: ${DEVELOPMENT_TEAM}
    CODE_SIGN_STYLE: Manual
    MARKETING_VERSION: "1.0.0"
targets:
  MyApp:
    type: application
    platform: iOS
    sources:
      - path: Sources
    resources:
      - path: Resources
    settings:
      base:
        PRODUCT_BUNDLE_IDENTIFIER: com.example.myapp
        GENERATE_INFOPLIST_FILE: YES
        ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon

팀 ID는 환경 변수로 들어오므로 저장소에 남지 않습니다. git에 프로젝트 파일이 없으니 pbxproj 병합 충돌도 없는데, 에이전트가 코드를 고치기 시작하면 은근히 이게 아주 중요해집니다.

2단계: Mac을 분 단위로 빌리기

Mac 쪽 작업은 전부 워크플로 파일 하나 .github/workflows/appstore.yml에 들어 있고, Windows에서 명령 하나로 시작합니다.

gh workflow run appstore.yml -f action=dry_run   # or compile, or release

compile은 빌드만 하고 멈춥니다. dry_run은 빌드, 서명, App Store Connect 업로드까지 하고 제출은 하지 않습니다. release는 심사 제출까지 합니다. 러너 관련 함정 두 개가 실제로 실행을 날렸고, 지금은 둘 다 파일에 주석으로 남아 있습니다.

함정 1: 있는데 없는 Xcode. macos-15 이미지에는 /Applications/Xcode_26.app가 존재했지만 안에 Contents/Developer가 없는 껍데기였습니다. 그래서 ls로는 멀쩡히 보이는데 선택하면 실패했습니다. 해결책은 실제로 툴체인이 들어 있는 가장 최신 Xcode 26을 고르는 것입니다.

chosen=""
for app in $(ls -d /Applications/Xcode_26*.app 2>/dev/null | sort -Vr); do
  if [ -d "$app/Contents/Developer" ]; then chosen="$app"; break; fi
done
sudo xcode-select -s "$chosen/Contents/Developer"

함정 2: 바뀌는 시뮬레이터. 기기 이름을 하드코딩했더니 여섯 번은 잘 되다가 일곱 번째에 "Unable to find a device matching the provided destination specifier"로 실패했습니다. 러너들이 매번 같은 시뮬레이터를 갖고 있지 않았기 때문입니다. 이제는 사용 가능한 가장 최신 iPhone을 고릅니다.

name=$(xcrun simctl list devices available \
       | grep -oE "iPhone [0-9]+ Pro Max" | sort -Vr | head -1)
echo "SIM_NAME=$name" >> "$GITHUB_ENV"

비용. GitHub 요금 안내 페이지에 따르면 공개 저장소의 표준 호스팅 러너에서 Actions는 무료입니다. 비공개 저장소에서는 3코어 또는 4코어 macOS 러너가 분당 $0.062입니다. 저희의 macOS 실행 322번(App Store, Build, 심사용 영상 워크플로)은 대기열부터 종료까지 합쳐 약 2,573분이었고, 비공개였다면 대략 $160이 들었을 겁니다. 앱 저장소가 공개라서 저희는 $0을 냈습니다. Chain 1.2(4분 22초) 같은 릴리스 한 번은 약 27센트입니다.

3단계: Mac 없이 서명하기(깨졌던 부분)

코드 서명은 빌드가 여러분에게서 나왔다는 걸 Apple이 아는 방법입니다. 인증서(Apple이 서명한 공개 키)와 그 개인 키, 그리고 이 둘을 앱에 묶는 프로비저닝 프로파일로 이루어집니다. Mac에서는 Xcode가 이걸 전부 숨겨 줍니다. CI 러너에서는 첫 앱이 오전 내내 여기서 시간을 보냈습니다.

2026년 9월 10일 GitHub Actions의 Glyphstorm App Store 워크플로 실행 목록 스크린샷: 오전 5:49의 10번 실행은 초록, 11번과 13~19번 실행은 실패, 12번은 취소, 오전 6:38 EDT의 20번 실행은 초록.
2026년 9월 10일 오전 5:49~6:38 ET의 Glyphstorm App Store 실행(github.com/Mattbusel/glyphstorm, 10월 16일 캡처).

9월 10일의 빨간 실행들이 실제로 남긴 메시지를 로그에서 그대로 가져왔습니다.

  • Your team has no devices from which to generate a provisioning profile. 자동 서명은 아카이브 중에 개발용 프로파일을 요청하는데, 개발용 프로파일에는 등록된 iPhone이 필요합니다. App Store 빌드에는 기기가 전혀 필요 없는데도 이 메시지를 받습니다.
  • Could not find the newly generated certificate installed. fastlane의 인증서 단계가 러너에서 새 인증서를 만들어 버렸습니다.
  • MAC verification failed during PKCS12 import (wrong password?). 비밀번호는 맞았습니다. 파일 형식이 문제였습니다.
  • Error resolving conflict between options: 'adhoc' and 'development'. 이 플래그를 false로 넘겨도 넘긴 것으로 칩니다.

두 번째가 비싼 실수입니다. CI가 인증서를 만들면 개인 키는 몇 분 뒤 삭제될 머신에서 태어납니다. Apple에는 아무도 다시 쓸 수 없는 인증서가 남고, Apple은 한 계정이 가질 수 있는 배포 인증서 수를 제한합니다. 누군가 알아차리기 전에 실패한 실행 세 번이 이미 인증서 세 개를 고아로 만들어 놓았습니다.

CI가 인증서를 만들게 두지 마세요. 키는 내 컴퓨터에서 한 번만 만들고, CI는 가져오기만 하게 하세요.

그래서 키는 Windows에서 만듭니다. Store/signing.py가 Python cryptography 라이브러리로 키를 생성하고, Apple의 인증서 생성 엔드포인트에 서명 요청을 보낸 뒤 .p12를 씁니다(일부 생략).

key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
csr = (x509.CertificateSigningRequestBuilder()
       .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Glyphstorm Distribution")]))
       .sign(key, hashes.SHA256()))
made = call("POST", "/v1/certificates", {"data": {"type": "certificates", "attributes": {
    "certificateType": "DISTRIBUTION",
    "csrContent": csr.public_bytes(serialization.Encoding.PEM).decode()}}})

# Legacy PKCS#12 (3DES + SHA-1). macOS `security import` can't read the AES-256 form
# and reports it as "MAC verification failed ... (wrong password?)".
encryption = (PrivateFormat.PKCS12.encryption_builder()
              .key_cert_algorithm(pkcs12.PBES.PBESv1SHA1And3KeyTripleDESCBC)
              .hmac_hash(hashes.SHA1())
              .build(P12_PASSWORD.encode()))

스크립트는 base64로 인코딩한 .p12를 바로 gh secret set에 넘기므로, 실수로 커밋될 수 있는 곳에는 절대 남지 않습니다. API 호출에는 .p8로 서명한 수명이 짧은 ES256 토큰을 씁니다. Apple은 20분보다 오래 사는 토큰을 거부하므로 호출할 때마다 새로 발급합니다.

러너에서는 일회용 키체인을 만들어 가져오고, 모든 CI 서명 가이드가 굵은 글씨로 써 둬야 할 한 줄을 실행합니다.

security import "$RUNNER_TEMP/dist.p12" -k "$keychain" \
  -P "$DIST_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
# Without the partition list, codesign blocks on a GUI prompt that
# nobody can answer and the job hangs until it times out.
security set-key-partition-list -S apple-tool:,apple:,codesign: \
  -s -k "" "$keychain" >/dev/null

(모든 플래그가 궁금하다면 security 명령어 레퍼런스를 참고하세요.) 그다음 빌드는 get_provisioning_profile로 App Store 프로파일을 직접 요청하고 수동으로 서명합니다. 그러면 기기가 없는 팀에서는 성공할 수 없는 개발용 프로파일 단계를 건너뛸 수 있습니다. 싸움이 시작된 지 실패 8번, 37분 만에 오전 6:38 실행이 초록이 되었고, 9월 10일 오전 6:43 ET에 로그에 Successfully submitted the app for review!가 찍혔습니다. 그 앱의 첫 커밋은 오전 4:06이었습니다.

4단계: 지루한 부분은 fastlane이 합니다

fastlane은 Apple의 빌드와 업로드 단계를 "레인(lane)"으로 감싸 주는 Ruby 도구입니다. app_store_connect_api_key로 secret에서 API 키를 읽기 때문에 Apple ID 비밀번호나 2단계 인증 창이 어디에도 나오지 않습니다. 여기에도 함정이 하나 더 있습니다. 빌드 번호입니다. 같은 빌드 번호를 두 번 올리면 Apple이 거부하므로, 매 빌드마다 App Store Connect에 최신 번호를 물어보고 1을 더합니다(fastlane/Fastfile).

next_build = latest_testflight_build_number(
  api_key: api_key,
  app_identifier: APP_IDENTIFIER,
  initial_build_number: 0
) + 1

이 값은 빌드 설정(CURRENT_PROJECT_VERSION=#{next_build})으로 넣고, increment_build_number는 쓰지 않습니다. 후자는 Apple의 agvtool을 쓰는데, 생성된 프로젝트에서 Cannot find "MyApp.xcodeproj/../YES"로 실패했습니다. 그다음 upload_to_app_store가 바이너리, 텍스트, 스크린샷을 보내며, submit_for_review는 release일 때 true, dry_run일 때 false입니다.

이 모든 게 갖춰지면 릴리스는 지루해집니다. 그게 목표입니다.

Chain 1.2의 GitHub Actions 작업 스크린샷: Xcode 26 선택 4초, xcodegen과 fastlane 설치 7초, 시뮬레이터 선택 25초, 컴파일 52초, 서명 키 기록 0초, 서명 인증서 가져오기 1초, 빌드, 업로드, 제출 2분 31초. 작업은 4분 9초 만에 성공.
Chain 1.2, 2026년 10월 8일: 작업 시간 4분 9초, 대기열 포함 4분 22초. 단계 이름은 Build, upload and submit이지만 이번엔 dry_run이라 업로드 후 멈췄습니다.

Mac 없이 App Store 스크린샷 찍기

Apple은 6.9인치 크기(1320 x 2868)의 스크린샷을 요구합니다. Mac이 없으면 시뮬레이터를 클릭할 수도 없으니, 앱이 실행 인자를 받아 데모 데이터와 함께 원하는 화면을 열도록 합니다. Chain의 Sources/App.swift에서 가져온 코드입니다.

func applyShotArgs(_ s: Store) {
    let a = ProcessInfo.processInfo.arguments
    guard let i = a.firstIndex(of: "-shot"), i + 1 < a.count else { return }
    switch a[i + 1] {
    case "year": tab = .year
    case "stats": tab = .stats
    case "rescue": rescue = s.rescues().first
    case "themes": shop = true
    default: break
    }
}

그러면 Build 워크플로가 새 시뮬레이터를 만들고, 상태 표시줄을 전통의 9:41과 배터리 가득으로 맞춘 뒤 반복합니다.

for shot in today widgets year detail rescue themes week stats edit paywall onboarding done; do
  xcrun simctl terminate "$udid" com.mattbusel.chainhabits || true
  xcrun simctl launch "$udid" com.mattbusel.chainhabits -shot $shot
  sleep 8
  xcrun simctl io "$udid" screenshot "shots/$(printf %02d $i)-$shot.png"
  i=$((i+1))
done

새 시뮬레이터가 중요합니다. 러너의 기본 시뮬레이터를 부팅했다가 15분 넘게 멈춘 적이 있습니다. 원본 PNG는 빌드 아티팩트로 돌아오고, Windows의 작은 스크립트가 각 이미지 위에 헤드라인을 얹습니다.

Chain의 App Store 스크린샷 4장: Five seconds a day, Watch your year fill in, Missed a day? Save the chain, Know your best days. 각각 라임색 습관 사각형이 있는 어두운 앱 UI를 보여 줍니다.
Chain의 App Store 스크린샷 10장 중 4장: CI 시뮬레이터에서 찍은 원본에 Windows의 스크립트로 헤드라인을 붙였습니다.

여기서 의견 하나. 실제로 나가는 스크린샷은 저장소에 커밋되고, release 레인은 다시 찍지 않습니다. Apple이 보는 이미지는 사람이 확인한 바로 그 이미지이고, 서명 문제로 재시도할 때마다 10분씩 줄어듭니다.

139번의 실행이 가르쳐 준 것

GitHub Actions API에서 모든 실행을 가져오면 깔끔한 학습 곡선이 나옵니다. 저장소 21개에서 App Store 워크플로 실행은 139번, 성공 81번, 실패 43번, 취소 15번이었습니다. 성공한 실행의 중앙값은 대기열부터 종료까지 약 8분이었습니다.

출시 순서대로 앱별 실패한 App Store 워크플로 실행 수 막대 차트: Glyphstorm 13, Clear the Strait 13, Pocket Beings 5, Minder 3, Fairway Ledger 2, Baseline Ledger 2, Ironbook 2, Race Fuel 1, Chain 1, Quiver 0, Rooms 1, 그 뒤 Clockout, Odometer, Curve, Pricebook, Pawprint, Chores, Climbbook, Cutoff, Parked, Race Pace는 모두 0.
앱별 실패한 App Store 워크플로 실행 수, 전체 기간, 첫 실행 순서대로(2026년 9월 10일~10월 8일). 출처: GitHub Actions API, 2026년 10월 16일 수집.

처음 두 앱인 Glyphstorm과 Clear the Strait는 각각 13번씩 실패했고, 대부분 9월 10일에 몰렸습니다. 하루 늦게 시작한 Pocket Beings는 총 5번이었습니다. 9월 24일 묶음부터는 대부분 0이었고, 마지막 10개 앱은 App Store 실행 실패 없이 출시되었습니다. Climbbook, Cutoff, Parked, Race Pace는 9월 27일에 각자 첫 실행으로 App Store Connect까지 갔습니다. 같은 파이프라인, 파일 안의 같은 주석. 모든 수정 사항이 다음 앱이 읽을 자리에 기록되어 있었습니다.

그런데 반전이 있습니다. 실패가 0이었던 바로 그 앱들이 App Review에서 반려된 앱들입니다. 10월 5일, Apple은 그중 11개를 Guideline 4.3(스팸)과 4.2.6으로 리젝했습니다. 제 해석은 이렇습니다. 출시 비용이 0이 되면 병목은 CI에서 "이 앱이 스토어에 올라갈 만큼 충분히 다른가"로 옮겨 갑니다. 그 질문에는 파이프라인이 답해 줄 수 없습니다. 제품이 답해야 합니다.

새 계정이라면 첫 제출에서 Guideline 2.1 "Information Needed"도 예상해야 합니다. 저희의 첫 두 앱 모두 받았습니다. 짧은 앱 화면 녹화로 답할 수 있고, 같은 러너가 그 녹화도 할 수 있습니다.

오늘 여러분의 프로젝트에서 할 일

  1. git에서 .xcodeproj를 지우고 XcodeGen용 project.yml을 작성하세요. 프로젝트는 CI에서 생성합니다.
  2. 인증서는 내 컴퓨터에서 API로 만들고, 레거시 형식 .p12로 저장해 GitHub secret에 넣으세요. CI가 이미 만든 인증서가 있다면 목록을 확인해 고아 인증서를 폐기하세요.
  3. CI에서는 일회용 키체인, 가져오기, set-key-partition-list. 마지막을 빼먹으면 100% 멈춥니다.
  4. App Store 프로파일로 수동 서명하세요. 자동 서명이 없는 기기를 찾아 헤매게 두지 마세요.
  5. 빌드 번호 = 최신 TestFlight 빌드 + 1, 빌드 설정으로 넘기세요.
  6. Xcode 경로나 시뮬레이터 이름을 하드코딩하지 마세요. 실행 시점에 실제로 존재하는 가장 최신 것을 고르세요.
  7. -shot 실행 인자를 추가해 CI가 App Store 스크린샷을 찍을 수 있게 하세요.
  8. 상품 페이지가 제대로 보일 때까지 dry_run을 쓰고, 그다음 release하세요.

이 방식으로 SwiftUI 앱을 만든다면, 이 파이프라인에서 나온 앱 중 하나인 Minder의 스프링 애니메이션 글도 참고하세요. 그리고 코딩 에이전트가 CI 루프를 대신 돌리고 있다면 코딩 에이전트가 테스트를 고치지 못하게 막는 법과 PC 한 대에서 에이전트 20개를 돌리며 깨진 것들을 먼저 읽으세요. 새벽 6시에 서명을 고치는 에이전트는 기꺼이 검사 쪽을 "고쳐" 버리니까요.

전체 파이프라인(모든 함정에 주석을 단 워크플로, Fastfile, signing.py, App Store Connect 스크립트, 이를 단계별로 안내하는 에이전트 스킬)은 Ship iOS from Windows kit로 묶어 두었습니다. 앱 자체를 누군가 만들어 주길 원하신다면 저희의 고정 가격은 여기 있습니다.

아직도 Mac이 꼭 필요하다고 생각하는 Apple 단계가 하나 있다면 무엇인가요? 알려 주시면 API로 가능한지 확인해 보겠습니다.

2026년 10월 16일 tensorust.com에 처음 게시되었습니다. X(@TensorustAI)와 dev.to에도 실렸습니다. 관련 링크: 이 파이프라인을 여러분의 저장소에 구축해 드립니다: $299, 고정 가격 iPhone 앱 개발, iPhone 앱 목록, Ship iOS from Windows kit.

다음 읽을거리

전체 아티클 →

한국어로 더 읽기

한국어: Tensorust →

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ