버전은 올렸는데 왜 또 거부당하죠? (마케팅 버전과 빌드 번호는 다른 숫자입니다)

저는 모바일 개발자입니다. 그런데도 한동안 Info.plist에 숫자가 왜 두 개나 들어있는지 헷갈려 했습니다(이거 말하면 혼나려나요...). CFBundleShortVersionString이 있고, CFBundleVersion이 있는데, 둘 다 "버전"이라고 불러서 그런 것 같아요. 이름이 비슷하면 사람이 헷갈리는 건 당연하잖아요ㅎㅎ
결론부터 말씀드리면, 이 둘은 완전히 다른 질문에 대한 답입니다.
두 숫자는 서로 다른 질문에 답한다
- 마케팅 버전 (
CFBundleShortVersionString):2.1.1처럼 앱을 쓰는 사람에게 보여 주는 버전입니다. Apple 문서도 이 값을 "앱 사용자에게 보이는 숫자"라고 설명합니다[1]. "사용자 입장에서 이게 새로운 릴리즈인가?"에 답합니다. - 빌드 번호 (
CFBundleVersion): 같은 버전 안에서 빌드 하나하나를 구분하는 숫자입니다[5]. "이게 새로운 업로드인가?"에 답합니다.
안드로이드도 똑같은 구조입니다. versionName이 마케팅 버전, versionCode가 빌드 번호 역할을 합니다. Android 문서는 아예 versionCode를 "사용자에게 보여 주는 버전 번호가 아니다"라고 못 박고, 사용자에게 보이는 값은 versionName 하나뿐이라고 설명합니다[2].
fastlane도 이 둘을 별개의 액션으로 다룹니다. increment_version_number는 마케팅 버전을, increment_build_number는 빌드 번호를 올립니다[3][4]. 소스 코드를 열어 보면 increment_build_number 안에 "이건 버전 번호가 아니라 빌드 번호다"라는 주석이 대놓고 붙어 있을 정도예요. 다들 한 번씩 헷갈렸던 거죠(?).
그래서 왜 거부당하는 걸까
제목의 상황은 대개 이렇게 생깁니다. 마케팅 버전은 그대로 두고 빌드를 다시 올렸는데, 빌드 번호를 안 올렸거나 오히려 낮은 번호가 들어간 경우입니다.
조금 오래된 기술 문서이긴 하지만, Apple은 iOS 앱의 빌드 번호가 같은 버전(릴리스 트레인) 안에서 고유해야 하고, 새 빌드는 증가하는 순서여야 한다고 정해 두었습니다. 반대로 서로 다른 버전끼리는 빌드 번호가 겹쳐도 괜찮습니다[5]. 그래서 같은 2.1.1 안에서 이미 올린 빌드보다 높지 않은 번호로 다시 올리면, "빌드 버전이 그 트레인의 최신 빌드보다 높아야 한다"는 오류와 함께 업로드가 거부됩니다[5].
안드로이드도 비슷합니다. 이미 사용한 versionCode로는 Play 스토어에 업로드할 수 없습니다[2].
반대 방향의 실수도 있습니다. 새 기능이 들어간 릴리즈인데 빌드 번호만 올리고 마케팅 버전을 그대로 두는 경우입니다. 사용자에게 보이는 건 마케팅 버전뿐이라서[1][2], 사용자는 뭐가 달라졌는지 버전만 보고는 알 수가 없습니다.
둘 다 "어차피 숫자 하나 올리는 거 아닌가?"라는 생각에서 시작되는 실수입니다. 근데 아니었던 거죠(?).
그래서 다음 빌드 번호는 어디서 가져와야 하나
여기서 한 발 더 들어가면, "다음 빌드 번호를 대체 어디서 가져와야 하는가"라는 질문이 남습니다. 로컬 작업 복사본? git 히스토리? 저장소에 카운터 파일을 둘까?
제가 좋아하는 답은 "그 셋 다 아니고, 스토어한테 직접 물어보라"입니다. 업로드를 거부하는 쪽이 스토어니까, 마지막 번호도 스토어가 제일 정확하게 알고 있겠죠. Atomic Object의 글이 fastlane으로 이 패턴을 정리해 두었습니다[6]:
previous_build_number = latest_testflight_build_number(app_identifier: app_id, api_key: api_key)
current_build_number = previous_build_number + 1
increment_build_number(build_number: current_build_number)
latest_testflight_build_number는 TestFlight에 올라간 빌드 중에서 버전 번호를 기준으로 가장 최근 빌드 번호를 가져오는 액션입니다[7].
안드로이드도 짝이 되는 코드가 있습니다:
google_play_track_version_codes(track: "internal", ...)[0] + 1
increment_version_code(...)
google_play_track_version_codes는 지정한 트랙의 version code를 배열로 돌려줍니다[8]. 그리고 increment_version_code는 fastlane 기본 액션이 아니라 서드파티 플러그인(fastlane-plugin-increment_version_code)이라서, 따로 설치해야 동작합니다[9]. 원문 글은 이 점을 따로 짚지 않으니 따라 하실 때 참고하세요.
이 방식이 좋은 이유를 제 나름대로 정리해 보면 세 가지입니다.
- 기기와 CI 세션을 넘나드는 연속성. 로컬이든 CI 워커든, "마지막 빌드 번호가 몇이었는지" 아무도 기억할 필요가 없습니다. 스토어가 공유된 진실의 원천이니까요.
- git 상태에 의존하지 않음. 번호를 커밋 수에서 뽑지 않으니, 스쿼시 머지나 강제 푸시로 히스토리가 바뀌어도 시퀀스가 흔들리지 않습니다.
- 플랫폼 독립성. iOS와 안드로이드는 각자 다른 스토어에 물어보기 때문에, 두 시퀀스가 서로 다르게 진행돼도 문제가 없습니다.
그런데, 공짜는 아닙니다
몇 가지 주의할 점도 있습니다.
- 자격 증명이 필요합니다. iOS 쪽은 App Store Connect API 키(
api_key)[7], 안드로이드 쪽은 Google Play 서비스 계정 JSON 키(json_key)[8]가 있어야 저 두 쿼리가 동작합니다. 이 키를 재현 가능하게 발급하고 관리하는 방법은 아직 제가 직접 정리해 보지 않아서, 이 글에서는 다루지 않겠습니다. - 트랙을 잘못 고르면 역행합니다.
google_play_track_version_codes는 지정한 트랙 하나의 코드만 돌려줍니다[8]. production 트랙에 더 높은 번호가 있는데 internal 트랙만 조회하면, 다음 빌드 번호가 오히려 낮게 나올 수 있습니다. 위 예시가 배열의 최댓값이 아니라 첫 번째 원소([0])를 쓴다는 점도 함께 기억해 두면 좋습니다. - 네트워크에 의존합니다. 두 쿼리 모두 원격 API를 호출하므로, 일시적인 장애가 나면 번호를 올리기 전에 레인이 멈춥니다. 숫자를 멋대로 지어내지 않는다는 점에서는 오히려 바람직한 실패지만, 이 레인이 오프라인에서는 못 돌아간다는 뜻이기도 합니다.
마케팅 버전을 xcconfig 파일로 관리하고 있다면 하나 더 알아 두시면 좋습니다. Jesse Squires는 fastlane이 xcconfig를 쓰는 프로젝트를 "지원하지 않는 것 같다"고 정리하고, 서드파티 플러그인 fastlane-plugin-xcconfig의 get_xcconfig_value / update_xcconfig_value로 MARKETING_VERSION 키를 직접 읽고 쓰는 방법을 소개합니다[10][11].
정리
결국 핵심은 하나입니다. "사용자가 보는 숫자"와 "스토어가 업로드를 구분하는 숫자"는 각자 다른 질문에 답하니, 각자 다른 방식으로 관리해야 한다. fastlane이 두 액션을 나눠 놓은 것도, 다음 빌드 번호를 로컬 상태 대신 스토어에 묻는 패턴도, 결국 같은 원칙에서 나온 선택입니다.
여러분의 릴리즈 스크립트는 이 둘을 어떻게 구분하고 계신가요? 혹시 저와 똑같이 처음엔 헷갈리셨다면, 댓글로 몰래 공감해주세요(ㅎㅎ).
참고 자료
- Technical Q&A QA1827: Automating Version and Build Numbers Using agvtool — Apple
- Version your app — Android Developers
increment_version_number— fastlane 공식 문서increment_build_number— fastlane 공식 문서- Technical Note TN2420: Version Numbers and Build Numbers — Apple
- How to Increment Version and Build Numbers with fastlane — Atomic Object Spin
latest_testflight_build_number— fastlane 공식 문서google_play_track_version_codes— fastlane 공식 문서- fastlane-plugin-increment_version_code — GitHub
- Using fastlane to increment version numbers in xcconfig files — Jesse Squires
- fastlane-plugin-xcconfig — GitHub
다음 글도 받아보세요.
글을 끝까지 읽으셨다면, 다음 글은 받은편지함이나 RSS 리더에서 만나보세요.