Expo 앱은 만들었는데 출시가 안 됩니다: `eas build`보다 먼저 준비할 것들
Expo를 사용하면 네이티브 빌드 과정이 크게 단순해집니다. 하지만 앱스토어 출시까지 명령어 한두 줄로 끝나는 것은 아닙니다. 실제 자동 배포를 만들려면 앱 등록, 서명 자격 증명, 스토어 API 권한, CI/CD 승인 절차가 먼저 준비되어야 합니다.
이 글은 새로운 Expo 프로젝트를 시작한다는 가정 아래, iOS와 Android 앱을 빌드하고 스토어 심사까지 연결할 때 꼭 알아야 할 항목을 정리한 실전 요약입니다. 예시는 Expo SDK 56과 EAS를 기준으로 합니다.
먼저 구분해야 할 네 단계
모바일 배포 자동화는 다음 네 단계로 나누어 생각하면 이해하기 쉽습니다.
- 빌드: iOS의 IPA, Android의 AAB 같은 설치·제출 파일을 만듭니다.
- 업로드: 빌드 결과물을 App Store Connect 또는 Google Play Console에 올립니다.
- 심사 제출: 스토어 정보와 심사 답변을 확인하고 검토를 요청합니다.
- 출시: 승인된 버전을 즉시 또는 단계적으로 사용자에게 공개합니다.
eas build는 첫 번째 단계를, eas submit은 주로 두 번째 단계를 담당합니다. 특히 iOS에서 TestFlight에 업로드되었다고 해서 App Store 심사가 자동으로 시작되는 것은 아닙니다. Android도 트랙 업로드와 프로덕션 공개를 구분해야 합니다.
시작 전에 필요한 계정과 등록
공통
- Expo 계정과 EAS 프로젝트
- 소스 저장소와 CI 실행 환경
- 앱마다 고유한 iOS Bundle ID와 Android Package Name
- 버전 번호와 빌드 번호를 올리는 규칙
iOS
- Apple Developer Program 가입
- Apple Developer의 App ID 등록
- App Store Connect의 앱 레코드 생성
- 배포 인증서와 프로비저닝 프로파일
- 자동 업로드용 App Store Connect API Key
App Store Connect API Key는 보통 Issuer ID, Key ID, .p8 개인 키 파일로 구성됩니다. 이 키는 앱 서명용 인증서와 역할이 다릅니다. EAS가 자격 증명을 관리하도록 맡기더라도 두 종류의 권한을 구분해서 보관해야 합니다.
Android
- Google Play Console 개발자 계정
- Play Console의 앱 등록
- Play App Signing 설정
- 앱 업로드용 keystore
- 자동 업로드용 Google Play 서비스 계정 JSON 키
Android에서도 google-services.json과 Play Console 업로드용 서비스 계정 JSON은 전혀 다른 파일입니다. 전자는 Firebase 연동 설정이고, 후자는 Google Play Developer API를 호출하기 위한 비밀 자격 증명입니다.
CI에 보관할 값과 보관하면 안 되는 값
다음 값은 저장소에 커밋하지 않고 EAS Secret 또는 CI의 암호화된 Secret에 보관해야 합니다.
EXPO_TOKEN- App Store Connect API의 Issuer ID와 Key ID
- App Store Connect API
.p8키 - Android keystore와 비밀번호
- Google Play 서비스 계정 JSON
반면 앱 식별자, 제출 대상 트랙, 빌드 프로필 이름처럼 공개되어도 문제가 없는 설정은 일반 변수로 관리할 수 있습니다.
권한은 가능한 한 작게 부여하고, 운영 배포 환경에는 승인 규칙을 두는 편이 안전합니다. 로그에 비밀 값이 출력되지 않는지도 반드시 확인해야 합니다.
Expo SDK 56 프로젝트의 기본 구성
SDK 56은 React Native 0.85, React 19.2.3을 사용하며 Node.js 20.19 이상이 필요합니다. 새 프로젝트에서는 먼저 버전을 고정하고 상태를 확인합니다.
npx expo-doctor
npx expo install --check
npx eas-cli@latest login
npx eas-cli@latest build:configure앱 식별자는 환경이 달라도 임의로 바뀌지 않도록 명시합니다.
// app.config.js
export default {
expo: {
name: "Example App",
slug: "example-app",
version: "1.0.0",
ios: {
bundleIdentifier: "com.example.app",
},
android: {
package: "com.example.app",
},
},
};EAS 프로필은 개발, 사내 검증, 스토어 배포를 분리하는 방식이 관리하기 좋습니다.
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": {
"ascAppId": "APP_STORE_CONNECT_APP_ID"
},
"android": {
"track": "internal"
}
}
}
}예시의 값은 그대로 사용하는 비밀 키가 아니라 프로젝트별로 교체해야 하는 자리표시자입니다.
실제 배포 흐름
iOS
npx eas-cli@latest build --platform ios --profile production
npx eas-cli@latest submit --platform ios --profile production제출이 성공하면 빌드가 App Store Connect와 TestFlight에 나타납니다. 이후 수출 규정, 개인정보 처리, 심사 연락처, 스크린샷, 설명, 심사 메모 등을 확인하고 App Review에 제출합니다. 이 마지막 단계는 자동화할 수 있지만, 제품 정보와 법적 답변까지 자동으로 결정해서는 안 됩니다.
Android
npx eas-cli@latest build --platform android --profile production
npx eas-cli@latest submit --platform android --profile production처음에는 내부 테스트 트랙으로 업로드하고 실제 기기에서 설치·업데이트·로그인·결제를 확인하는 것을 권장합니다. 같은 versionCode의 AAB는 다시 올릴 수 없으므로 모든 스토어 빌드에서 값을 증가시켜야 합니다.
Google Play의 신규 개인 개발자 계정에는 프로덕션 액세스 전에 별도의 테스트 요건이 적용될 수 있습니다. 현재 공식 안내에는 대상 계정이 최소 12명의 테스터가 14일 연속 참여한 비공개 테스트를 거친 뒤 프로덕션 액세스를 신청하도록 설명되어 있습니다. 계정 유형과 정책 변경 여부는 실제 제출 전에 Play Console에서 다시 확인해야 합니다.
CI/CD에서 자동화할 부분
안전한 기본선은 다음과 같습니다.
- Pull Request: 타입 검사, 린트, 테스트
- 기본 브랜치 병합: preview 빌드 또는 내부 배포
- 버전 태그: production 빌드
- 승인 후: TestFlight와 Google Play 내부 트랙 업로드
- 담당자 확인 후: 스토어 심사 제출과 공개
EAS Workflows 또는 GitHub Actions 모두 사용할 수 있습니다. 중요한 것은 도구보다 환경 분리와 승인 경계입니다. 프로덕션 단계에는 보호 환경을 적용하고, 같은 커밋에서 검증된 결과물만 제출해야 합니다.
자주 놓치는 부분
- TestFlight 업로드와 App Store 심사 제출을 같은 작업으로 오해합니다.
google-services.json을 Google Play API 키로 착각합니다.- iOS 서명 인증서와 App Store Connect API Key의 용도를 혼동합니다.
- 앱의 버전, iOS 빌드 번호, Android
versionCode를 같은 값으로만 관리합니다. - 스토어 메타데이터와 개인정보 관련 답변을 준비하지 않은 채 제출 자동화부터 만듭니다.
- 비밀 키를 저장소, 빌드 로그, 캐시 또는 산출물에 포함합니다.
- Android 내부 테스트를 생략하고 곧바로 프로덕션 공개를 시도합니다.
DUOLABS의 권장 기준
저희는 모바일 배포를 “빌드 명령 자동화”가 아니라 “릴리스 판단을 재현 가능한 절차로 만드는 일”로 봅니다. 따라서 새 프로젝트에서는 다음 기준부터 적용하는 편이 좋습니다.
- 개발·검증·운영 환경과 앱 식별자를 명확히 구분합니다.
- 키의 소유자, 용도, 만료일, 교체 절차를 기록합니다.
- iOS와 Android 모두 내부 배포를 통과한 빌드만 심사에 제출합니다.
- 운영 제출에는 승인 단계를 두고 감사 가능한 로그를 남깁니다.
- Firebase와 푸시 알림은 별도 확장 단계로 두되, 필요한 APNs 키와 Firebase 설정의 책임 범위를 미리 문서화합니다.
이 구조를 먼저 마련하면 새 앱에서도 담당자의 기억에 의존하지 않고 동일한 품질로 배포할 수 있습니다.
공식 문서
함께 읽기
- Flutter 앱 배포, `flutter build` 한 줄이면 끝인 줄 알았습니다Flutter 앱의 빌드 명령은 간단합니다. Android는 flutter build appbundle, iOS는 flutter build ipa로 결과물을 만들 수 있습니다. 그러나 팀이 반복해서 사용할 수 있는 배포 체계를 만들려면 그 뒤에 있는 서명, 권한, 스토어 API, 심사와 출시 절차까지 함께 설계해야 합니…
- 같은 Expo 앱인데 배포 방식은 달랐습니다: iOS와 Android 심사 제출 비교기앞선 글에서는 이미 TestFlight에 올라간 iOS 빌드를 AI가 App Store 심사까지 제출한 과정을 다뤘습니다.
- 코드는 그대로인데 앱 심사는 제출됐습니다: AI에게 Expo 배포를 맡겨본 기록코드를 한 줄도 수정하지 않았는데 TestFlight에 올라가 있던 앱이 App Review 제출 상태로 바뀌었습니다. 얼핏 보면 AI가 앱을 새로 빌드해서 배포한 것처럼 보이지만, 실제로는 이미 준비된 빌드와 스토어 정보를 확인한 뒤 App Store Connect의 심사 절차를 진행한 것입니다.
- 모바일 앱 스토어 배포 자동화를 설계할 때 볼 것들모바일 앱 배포는 빌드 파일을 만드는 것에서 끝나지 않습니다. 인증서, 서명 키, 스토어 메타데이터, 심사 정보, 버전 관리, 제출 자동화까지 함께 설계해야 반복 가능한 배포가 됩니다.
- Expo EAS 비용 구조와 절약 전략Expo Application Services(EAS)는 React Native 앱을 빌드, 제출, 업데이트하는 과정을 단순하게 만들어줍니다. 하지만 팀 규모와 빌드 빈도에 따라 비용 구조를 이해하고 써야 합니다.