업로드가 "성공"인데 App Store Connect에 빌드가 없다면 — plist 한 줄의 함정
iOS 앱을 올리는데 어떤 앱은 되고 어떤 앱은 안 됐습니다. 원인은 ExportOptions plist 딱 두 줄이었고, 그 파일이 build/ 폴더에 있어서 flutter clean 한 번에 사라지고 있었습니다. Transporter 없이 xcodebuild만으로 올리는 표준 절차를 정리했습니다.
안녕하세요. Jay입니다!
개인 앱을 여러 개 운영하다 보면 이상한 일을 겪습니다. 어떤 앱은 업로드가 잘 되고, 어떤 앱은 같은 방법인데 안 됩니다. 그때마다 "이 앱은 왜 이러지" 하면서 Transporter를 찾아보고, App Store Connect API 키를 발급받아야 하나 고민하다가, 어찌어찌 되면 그냥 넘어갔습니다.
며칠 전에 iOS 레포 12개를 한 번에 훑어보고 원인을 찾았습니다. 앱의 문제가 아니라 파일 하나가 있느냐 없느냐의 문제였습니다.
🔍 가장 나쁜 증상 — 실패가 성공처럼 보인다
먼저 이 함정부터 말씀드려야겠습니다. 원인을 찾는 데 가장 오래 걸린 이유이기 때문입니다.
xcodebuild -exportArchive를 돌리면 마지막에 이렇게 찍힙니다.
** EXPORT SUCCEEDED **
종료 코드도 0입니다. 그런데 App Store Connect에 들어가 보면 빌드가 없습니다.
실패한 게 아닙니다. 명령은 시킨 일을 정확히 했습니다. 다만 시킨 일이 "업로드"가 아니라 "IPA 파일을 만들어서 폴더에 놓기" 였을 뿐입니다. 그걸 가르는 게 옵션 파일의 한 줄입니다.
| 키 | flutter 기본값 | 업로드하려면 |
|---|---|---|
destination |
export |
upload |
export는 아카이브에서 IPA를 뽑고 끝냅니다. upload여야 xcodebuild가 전송까지 합니다. 그런데 두 경우 모두 EXPORT SUCCEEDED가 뜨니까, 로그만 봐서는 구분이 안 됩니다.
flutter build ipa는 이 파일을 자동으로 만들어 주는데 기본값이 export입니다. 당연한 선택입니다 — 대부분은 IPA만 필요하니까요. 문제는 그 파일을 그대로 -exportOptionsPlist에 넘기면 업로드가 안 되면서 성공처럼 보인다는 겁니다.
🧾 두 번째 함정 — 버전이 내 마음대로 안 올라간다
같은 파일에 이런 키도 있습니다.
| 키 | flutter 기본값 | 원하는 값 |
|---|---|---|
manageAppVersionAndBuildNumber |
true |
false |
true면 Xcode가 빌드 번호를 알아서 올려버립니다. pubspec.yaml에 version: 1.14.0+31이라고 적어두고 올렸는데 콘솔에는 다른 번호가 찍히는 경우가 이겁니다. false로 두면 프로젝트에 적은 값이 그대로 올라갑니다.
정리하면 flutter가 만들어 주는 plist와 업로드용 plist의 차이는 딱 이 두 줄입니다.
🎯 진짜 원인 — 그 파일이 build/ 안에 있었다
여기까지는 "그럼 파일을 고치면 되겠네"입니다. 그런데 제 상황은 조금 달랐습니다. 레포 12개를 뒤져보니 이랬습니다.
| 확인 | 결과 |
|---|---|
| 업로드용 plist를 가진 레포 | 12개 중 4개 |
| 그 4개의 파일 위치 | 전부 build/ 아래 |
build/가 어떤 폴더인지 생각해 보면 답이 나옵니다.
.gitignore대상입니다 → 커밋되지 않으니 다른 컴퓨터에는 존재하지 않습니다flutter clean이 지웁니다 → 빌드가 꼬여서 한 번 청소하는 순간 사라집니다
즉 한 번 지워지는 순간 그 앱은 "업로드가 안 되는 앱"이 됩니다. 앱마다 되고 안 되고가 갈렸던 이유가 정확히 이거였습니다. 최근에 flutter clean을 한 앱은 안 되고, 안 한 앱은 됐던 겁니다.
원인이 랜덤처럼 보였던 것도 당연합니다. 파일의 수명이 제 빌드 습관에 달려 있었으니까요.
🛠 그래서 어디에 두어야 하나
build/ 밖, git이 추적하는 경로에 두고 커밋하면 끝입니다. 비밀값이 들어가지 않는 파일이라 커밋해도 안전합니다.
어디에 둘지는 한 줄로 판별할 수 있습니다.
git check-ignore -q ios && echo "ExportOptions-upload.plist" || echo "ios/ExportOptions-upload.plist"
일반 Flutter 프로젝트는 ios/가 git에 들어 있으니 ios/ 아래에 둡니다. Expo CNG 프로젝트처럼 ios/ 자체가 prebuild 생성물이라 gitignore된 경우에는 레포 루트에 둡니다. 이미 다른 경로에 추적 중인 파일이 있다면 그대로 쓰면 됩니다 — 중요한 건 위치가 아니라 지워지지 않는 곳에 있느냐입니다.
파일 내용은 이렇습니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key> <string>app-store-connect</string>
<key>destination</key> <string>upload</string>
<key>teamID</key> <string>YOUR_TEAM_ID</string>
<key>signingStyle</key> <string>automatic</string>
<key>uploadSymbols</key> <true/>
<key>manageAppVersionAndBuildNumber</key> <false/>
</dict>
</plist>
📦 표준 절차 — 명령 두 개
파일을 고정해 두면 배포는 두 줄로 끝납니다.
# 1) 아카이브 + IPA 생성
flutter build ipa --release
# 2) 같은 아카이브를 App Store Connect로 바로 업로드
xcodebuild -exportArchive \
-archivePath build/ios/archive/Runner.xcarchive \
-exportOptionsPlist ios/ExportOptions-upload.plist \
-exportPath build/ios/upload \
-allowProvisioningUpdates
네이티브 Xcode 프로젝트라면 1번만 xcodebuild archive -scheme <스킴> -archivePath <경로>.xcarchive로 바꾸고 2번은 그대로입니다.
Transporter도, altool도, App Store Connect API 키도 쓰지 않습니다. 저는 셋 다 설치·발급하지 않은 상태인데 이 방법으로 잘 올라갑니다.
🔐 자격증명을 안 넘기는데 왜 통과할까
명령 어디에도 계정 정보가 없는데 인증이 되는 게 처음엔 이상했습니다. 두 가지가 따로 동작하고 있었습니다.
인증 — Xcode에 로그인해 둔 Apple ID 세션을 xcodebuild가 그대로 씁니다. 그래서 이 방법은 Xcode가 설치되고 로그인된 그 맥에서만 동작합니다. CI 서버나 원격 머신에서는 안 됩니다.
서명 — 클라우드 관리 서명(cloud managed signing)입니다. 제 맥 키체인에는 Apple Development 인증서 하나뿐이고 배포용 인증서가 없습니다. 애플 서버가 대신 서명해 주는 거죠. -allowProvisioningUpdates가 그 경로를 여는 플래그라서, 이 플래그를 빼면 서명 단계에서 실패합니다.
⚠️ 알아두면 좋은 실패 하나
프레임워크 서명 도중 이런 에러가 날 때가 있습니다.
404 (RESULTS_UNAVAILABLE)
애플 쪽 일시적인 오류입니다. 로컬 설정을 고치지 마세요. 같은 명령을 그대로 한 번 더 실행하면 대개 통과합니다. 저는 어떤 버전은 첫 시도에 실패하고 재시도에 통과했고, 다음 버전은 첫 시도에 바로 통과했습니다. 설정은 그 사이 아무것도 바뀌지 않았습니다.
💡 Jay의 정리 — 재현되지 않는 절차는 절차가 아니다
이번 일에서 제가 얻은 건 업로드 명령어보다 그 앞의 교훈이었습니다.
첫째, 성공 메시지를 결과 확인으로 착각하지 말 것. EXPORT SUCCEEDED는 "시킨 일을 했다"는 뜻이지 "내가 원한 일이 됐다"는 뜻이 아닙니다. 확인은 App Store Connect에 빌드가 보이느냐로 해야 합니다.
둘째, 빌드 산출물 폴더에 설정을 두지 말 것. build/, dist/, .next/ 같은 폴더는 언제든 통째로 지워지는 게 정상인 곳입니다. 거기에 설정이 있으면 그 설정은 지워지기 전까지만 유효한 설정입니다. 그리고 지워졌을 때 원인을 찾기가 아주 어렵습니다 — 아무것도 안 바꿨는데 안 되니까요.
셋째, 앱마다 방법이 다르다면 그건 앱 문제가 아닐 가능성이 높습니다. 저는 몇 달 동안 "이 앱은 이 방법으로 됐다"를 앱별로 기억하려고 했습니다. 실제로는 하나의 절차와 하나의 파일이 있었을 뿐이고, 그 파일이 어떤 레포에는 있고 어떤 레포에는 없었던 것뿐이었습니다.
결론
App Store Connect에 빌드가 안 보인다면 destination이 export로 되어 있는지 먼저 확인해 보세요. 로그가 성공이라고 말하고 있어도요.
그리고 업로드용 plist는 build/ 밖 git 추적 경로에 두고 커밋하세요. 파일 하나 커밋해 두는 것으로 "이 앱은 왜 안 되지"를 영구히 없앨 수 있습니다.
다음에도 유익한 포스팅으로 찾아오겠습니다. 감사합니다!