RSS

같은 Expo 앱인데 배포 방식은 달랐습니다: iOS와 Android 심사 제출 비교기

모바일 앱글: , Duolabs15분 읽기app-storeblogexpogoogle-playmodel-openai-gpt-5technical-note

앞선 글에서는 이미 TestFlight에 올라간 iOS 빌드를 AI가 App Store 심사까지 제출한 과정을 다뤘습니다.

이번에는 같은 Expo 앱의 Android 버전을 Google Play 프로덕션 트랙으로 제출했습니다. 두 플랫폼 모두 EAS로 빌드했지만, 스토어에 제출하는 방식은 생각보다 많이 달랐습니다.

이전 글: 코드는 그대로인데 앱 심사는 제출됐습니다

왜 이 차이를 알아야 할까요?

Expo를 사용하면 iOS와 Android의 빌드 명령은 거의 비슷해집니다. 그래서 스토어 제출 방식도 같을 것이라고 생각하기 쉽습니다.

하지만 EAS가 만든 파일이 스토어에 도착한 뒤부터는 플랫폼별 모델이 달라집니다. iOS는 App Store 버전에 TestFlight 빌드를 연결하고, Android는 업로드한 AAB를 출시 트랙에 배치합니다. 이 차이를 모르면 이미 등록된 Android 빌드를 다시 업로드하거나, EAS Submit이 App Review까지 끝내준다고 오해하기 쉽습니다.

먼저 한눈에 비교해 보겠습니다

구분 iOS Android
빌드 산출물 IPA AAB
테스트 배포 TestFlight Internal·Closed·Open testing
버전 식별 버전 + 빌드 번호 버전 이름 + versionCode
스토어 구조 App Store 버전에 빌드 연결 빌드를 출시 트랙에 배치
제출 인증 App Store Connect API 키 Google 서비스 계정
출시 보류 수동 출시 설정 관리형 게시
자동화 API App Store Connect API Google Play Developer API

Expo에서는 두 플랫폼 모두 EAS Build와 EAS Submit을 사용하지만, 그 이후의 스토어 모델이 다릅니다.

iOS에서는 빌드를 버전에 연결했습니다

iOS 작업을 시작했을 때 1.0.2 (7) 빌드는 이미 TestFlight에 올라가 있었습니다.

하지만 App Store용 1.0.2 버전은 아직 생성되지 않았습니다. 따라서 다음 작업이 필요했습니다.

TestFlight 빌드 확인
→ App Store 버전 1.0.2 생성
→ 빌드 7 연결
→ 언어별 업데이트 내용 입력
→ 심사 정보 확인
→ App Review 제출

eas submit은 IPA를 App Store Connect에 업로드해 TestFlight에서 사용할 수 있게 해줍니다. 하지만 App Review 제출까지 자동으로 완료해 주는 것은 아닙니다.

Expo 공식 문서에서도 EAS Submit 이후 App Store Connect에서 빌드를 선택하고 App Review에 제출하는 단계를 별도로 안내합니다. Expo iOS 제출 문서

Android에서는 빌드를 트랙으로 승격했습니다

Android는 시작 상태가 조금 달랐습니다.

프로덕션: 1.0.1 (versionCode 4)
내부 테스트: 1.0.2 (versionCode 5)

새 버전인 1.0.2가 이미 Google Play 내부 테스트 트랙에 배포되어 있었습니다. 따라서 새 AAB를 만들거나 다시 업로드할 필요가 없었습니다.

남은 작업은 내부 테스트의 versionCode 5를 프로덕션 트랙으로 승격하는 것이었습니다.

처음에는 EAS Submit을 다시 실행했습니다

프로젝트에는 다음과 비슷한 제출 설정이 있었습니다.

{
  "submit": {
    "production": {
      "android": {
        "serviceAccountKeyPath": "./secrets/google-play-service-account.json",
        "track": "internal"
      }
    }
  }
}

가장 최근 Android 빌드를 제출하려면 다음 명령을 사용할 수 있습니다.

npx eas-cli@latest submit \
  --platform android \
  --profile production \
  --latest

하지만 이번에는 Google Play에서 이미 같은 versionCode가 제출되었다는 응답이 돌아왔습니다.

이 오류를 보고 무조건 versionCode를 올려서 다시 빌드하면 안 됩니다. 실제 원인은 빌드 실패가 아니라, 해당 AAB가 이미 Google Play에 등록되어 있었기 때문입니다.

Android의 versionCode는 새 업로드를 식별하는 고유 값입니다. 이미 업로드한 versionCode 5를 다시 업로드할 수는 없지만, 기존 빌드를 다른 트랙으로 승격할 수는 있습니다.

EAS Submit과 트랙 승격은 다른 작업입니다

EAS Submit은 새로운 AAB를 Google Play에 업로드할 때 편리합니다. Expo SDK 56 기준으로 프로덕션 프로필의 기본 Android 산출물은 Play Store에 제출할 수 있는 AAB입니다. Expo Android 제출 문서

# 프로덕션 AAB 생성
npx eas-cli@latest build \
  --platform android \
  --profile production

# Google Play 제출
npx eas-cli@latest submit \
  --platform android \
  --profile production \
  --latest

하지만 이미 내부 테스트에 존재하는 빌드를 프로덕션으로 옮기는 작업은 신규 업로드와 다릅니다.

이 경우에는 다음 방법을 사용할 수 있습니다.

  • Google Play Console에서 직접 승격
  • Google Play Developer API로 트랙 변경
  • 새로운 versionCode로 다시 빌드한 뒤 프로덕션 트랙에 제출

코드가 달라지지 않았다면 기존 빌드를 승격하는 방식이 가장 자연스럽습니다.

Google Play Developer API는 Edit 단위로 동작합니다

Google Play Developer API의 배포 변경은 Edit이라는 트랜잭션 안에서 처리됩니다.

Edit 생성
→ 현재 트랙과 번들 조회
→ 프로덕션 트랙 수정
→ 변경사항 검증
→ Edit 커밋

실제로 사용되는 API 흐름은 다음과 같습니다.

POST /applications/{packageName}/edits
PUT /applications/{packageName}/edits/{editId}/tracks/production
POST /applications/{packageName}/edits/{editId}:commit

프로덕션 트랙에는 다음과 같은 출시 정보를 전달합니다.

{
  "track": "production",
  "releases": [
    {
      "name": "1.0.2",
      "status": "completed",
      "versionCodes": ["5"],
      "releaseNotes": [
        {
          "language": "ko-KR",
          "text": "라이트 모드에서 앱 시작 화면의 로고가 더 자연스럽게 보이도록 개선했습니다."
        }
      ]
    }
  ]
}

여기서 status: completed는 Google 심사가 완료됐다는 뜻이 아닙니다. 단계적 출시가 아닌 전체 출시 대상으로 트랙을 구성했다는 의미입니다.

Google의 검토와 실제 게시 상태는 별도로 진행됩니다.

기존 심사를 취소하지 않도록 방어했습니다

Google Play API는 이미 다른 변경사항이 심사 중인 상태에서 새 Edit을 커밋하면 기존 심사를 취소하고 다시 제출할 수 있습니다.

이번 자동화에서는 다음 옵션을 사용했습니다.

changesInReviewBehavior=ERROR_IF_IN_REVIEW

이미 심사 중인 변경사항이 있다면 기존 심사를 건드리지 않고 오류로 중단하도록 한 것입니다.

AI 배포 자동화에서는 성공적으로 제출하는 것만큼, 기존 심사와 출시 상태를 훼손하지 않는 방어 로직이 중요합니다. Google Play Edit 커밋 문서

실제 Android 제출 순서

이번 작업은 다음 순서로 진행했습니다.

  1. Git 작업 트리가 깨끗한지 확인했습니다.
  2. EAS의 최신 Android 프로덕션 빌드를 조회했습니다.
  3. 1.0.2 (versionCode 5) AAB가 정상적으로 완료되었는지 확인했습니다.
  4. Google 서비스 계정의 필수 필드가 준비되어 있는지 검사했습니다.
  5. EAS Submit으로 제출을 시도했습니다.
  6. 동일한 versionCode가 이미 제출됐다는 응답을 확인했습니다.
  7. Google Play API로 현재 트랙과 업로드된 번들을 조회했습니다.
  8. versionCode 5가 내부 테스트에 배포된 상태임을 확인했습니다.
  9. 기존 프로덕션에는 versionCode 4가 배포되어 있음을 확인했습니다.
  10. versionCode 5를 프로덕션 트랙으로 승격했습니다.
  11. 한국어와 영어 업데이트 내용을 등록했습니다.
  12. Edit을 커밋해 Google 심사 대상으로 제출했습니다.
  13. 프로덕션 트랙이 1.0.2 (5)로 변경되었는지 다시 검증했습니다.
  14. 임시 배포 스크립트를 삭제하고 Git 상태를 확인했습니다.

이 과정에서 새로운 AAB를 만들거나 소스 코드를 수정하지 않았습니다.

인증 방식도 서로 다릅니다

iOS

App Store Connect API 자동화에는 일반적으로 다음 정보가 필요합니다.

  • Key ID
  • Issuer ID
  • .p8 개인 키
  • Bundle Identifier
  • App Store Connect 앱 ID

Android

Google Play 자동화에는 다음 정보가 필요합니다.

  • Google Cloud 서비스 계정
  • 서비스 계정 JSON 개인 키
  • Google Play Console에서 부여한 앱 권한
  • Android package name

서비스 계정 JSON에는 개인 키가 포함되어 있으므로 Git에 커밋하면 안 됩니다. EAS에 업로드하거나 CI 비밀 저장소에서 관리해야 합니다.

출시를 보류하는 방식도 다릅니다

iOS는 App Store 버전의 출시 방식을 수동 출시로 설정할 수 있습니다. 심사가 승인된 뒤 사람이 출시 버튼을 눌러 공개할 수 있습니다.

Android는 Google Play Console의 관리형 게시 기능을 사용합니다.

  • 관리형 게시가 켜져 있으면 심사 승인 후에도 직접 게시할 때까지 대기합니다.
  • 관리형 게시가 꺼져 있으면 심사 승인 후 자동으로 공개될 수 있습니다.

관리형 게시 여부는 배포 자동화 스크립트 밖에서 결정되는 계정 설정이므로, 프로덕션 제출 전에 반드시 확인해야 합니다. Google Play 관리형 게시 안내

두 플랫폼에서 공통으로 지켜야 할 원칙

플랫폼은 달라도 안전한 자동화 원칙은 같습니다.

  • 먼저 현재 상태를 읽기 전용으로 조회합니다.
  • 앱 식별자와 대상 버전을 고정해서 검증합니다.
  • 새로운 빌드가 정말 필요한지 확인합니다.
  • 동일한 버전을 중복 제출하지 않습니다.
  • 기존 심사 중인 변경사항을 취소하지 않습니다.
  • 개인정보와 정책 관련 답변을 AI가 추측하지 않도록 합니다.
  • 인증정보를 출력하거나 저장소에 커밋하지 않습니다.
  • 제출 후 스토어 상태를 다시 조회합니다.
  • 실제 공개 시점은 별도의 승인 단계로 분리합니다.

듀오랩스가 보는 관점

Expo를 사용하면 iOS와 Android의 빌드 과정은 상당히 비슷해집니다.

소스 확인
→ EAS Build
→ EAS Submit

하지만 스토어에 도착한 이후부터는 완전히 다른 모델을 이해해야 합니다.

iOS
TestFlight 빌드
→ App Store 버전 생성
→ 빌드 연결
→ App Review 제출

Android
내부 테스트 빌드
→ 프로덕션 트랙 승격
→ Edit 커밋
→ Google Play 검토

이번 Android 배포에서 가장 중요한 판단은 오류가 발생했을 때 곧바로 새 빌드를 만들지 않은 것이었습니다.

같은 versionCode가 이미 제출됐다는 응답은 실패한 빌드가 아니라, 이미 Google Play에 도착한 빌드가 있다는 신호였습니다. 현재 트랙을 확인하고 기존 빌드를 프로덕션으로 승격하면서 불필요한 빌드와 버전 증가를 피할 수 있었습니다.

AI에게 배포를 맡긴다는 것은 명령어를 대신 입력하게 하는 것만을 의미하지 않습니다. 현재 상태를 읽고, 플랫폼별 배포 모델을 이해하고, 가장 안전한 다음 동작을 선택하도록 만드는 것이 진짜 자동화에 가깝습니다.

참고 문서