Play 스토어 등록정보 자동화: EAS가 덮지 않는 한 칸
앱에 언어 셋을 더하고 스토어에 올리려던 참이었습니다. iOS 쪽은 명령 하나로 끝났습니다.
npx eas-cli metadata:pushstore.config.json 에 적어 둔 아홉 언어의 제목·설명·키워드가 그대로 App Store Connect 로 갔습니다. 스크린샷 24장까지 함께요.
Play 는 그 명령이 없습니다. EAS 의 metadata 기능은 App Store 전용이고, 실행할 때 스스로 이렇게 경고합니다.
EAS Metadata is in beta and subject to breaking changes.자격증명이 아니라 기능의 부재
처음에는 EAS 가 구글 쪽 권한을 안 갖고 있어서라고 생각했습니다. 아니었습니다. 바이너리 제출 로그를 보면 이렇게 찍힙니다.
Google Service Account Key:
Key Source : EAS servers
Account Email: <서비스 계정>@….iam.gserviceaccount.com같은 키가 EAS 에 이미 있습니다. 빌드도 제출도 EAS 가 그 키로 합니다. 없는 것은 키가 아니라 "Play 등록정보를 올려 주는 기능" 하나입니다.
그래서 그 칸만 직접 채우게 됩니다. 저희 리포에는 tools/play/ 아래 스크립트 넷이 그 일을 합니다. Google Play Developer API 를 직접 부르는 200줄이 안 되는 코드입니다.
| 파일 | 하는 일 |
|---|---|
api.mjs |
서비스 계정으로 토큰을 받고 REST 를 부른다 |
push.mjs |
등록정보(제목·설명·스크린샷)를 올린다 |
release.mjs |
업로드된 AAB 를 프로덕션 트랙으로 올린다 |
tracks.mjs · inspect.mjs |
지금 상태를 읽는다 |
확장자가 .mjs 인 것은 취향이 아닙니다. 이 앱은 React Native 라 package.json 에 "type": "module" 이 없는데, 이 스크립트들은 최상위 await 를 씁니다.
const tok = await token();
const edit = await api(tok, '/edits', { method: 'POST', body: {} });CommonJS 에서는 안 되는 문법입니다. 확장자를 .mjs 로 두면 package.json 설정과 무관하게 항상 ES 모듈로 읽히므로, 앱 설정을 건드리지 않고 도구 스크립트만 ESM 으로 쓸 수 있습니다.
같은 표를 각자 들고 있던 두 스크립트
언어를 늘리려고 push.mjs 를 열었더니 이런 상수가 있었습니다.
const PLAY_LOCALE = {
ko: 'ko-KR', en: 'en-US', ja: 'ja-JP',
'zh-Hans': 'zh-CN', 'zh-Hant': 'zh-TW', es: 'es-ES',
};Play 의 언어 코드는 App Store 와도, 앱 안에서 쓰는 코드와도 다릅니다. 앱은 zh-Hans, App Store 도 zh-Hans, Play 는 zh-CN 입니다. 그래서 변환표가 필요합니다.
여기에 세 줄을 더하고 등록정보를 올렸습니다. 잘 됐습니다.
그다음 프로덕션 승격 스크립트를 열었더니 같은 표가 또 있었습니다.
// release.mjs
const PLAY_LOCALE = { ko: 'ko-KR', en: 'en-US', ja: 'ja-JP', 'zh-Hans': 'zh-CN', 'zh-Hant': 'zh-TW', es: 'es-ES' };제가 고친 것은 한쪽뿐이었습니다.
안 고친 쪽이 조용히 실패하는 구조
push.mjs 에는 이런 줄이 있습니다.
const lang = PLAY_LOCALE[locale];
if (!lang) throw new Error(`Play 언어 코드가 없다: ${locale}`);표에 없는 언어를 만나면 멈춥니다. 그래서 만약 제가 push.mjs 를 안 고쳤다면 그 자리에서 알았을 겁니다.
release.mjs 에는 그 줄이 없었습니다.
return { language: PLAY_LOCALE[loc], text: c.releaseNotes };표에 없으면 language: undefined 가 그대로 API 로 갑니다. 실패하면 다행이고, 통과하면 이상한 데이터가 스토어에 올라갑니다. 어느 쪽인지는 돌려 봐야 아는데, 돌리는 순간이 곧 실제 사용자에게 나가는 순간입니다.
두 파일이 갈라진 것이 문제의 절반이고, 갈라진 채로 한쪽에만 방어가 있던 것이 나머지 절반입니다. 표를 api.mjs 한 곳으로 옮기고 없는 쪽에 같은 예외를 넣었습니다.
한 번 더 반복한 같은 실수
승격 스크립트를 마저 읽다가 출시 노트를 어디서 읽는지 봤습니다.
const COPY = 'store-assets/1.1.0/copy';거기 있던 여섯 개 파일을 열어 보니 이렇게 적혀 있었습니다.
Now available in six languages.
6개 언어를 지원합니다.이전 버전의 문안이었습니다. 아홉 언어를 더한 릴리스에 「6개 언어」라고 적힌 출시 노트가 나갈 뻔했습니다.
원인이 앞의 것과 같습니다. App Store 용 문안은 store.config.json 에, Play 용 문안은 store-assets/…/copy/*.json 에 따로 있습니다. 저는 새 언어 셋을 추가하면서 App Store 쪽을 고치고, Play 쪽은 새로 만드는 세 개만 만들었습니다. 기존 여섯 개는 손대지 않았고, 그래서 낡은 채로 남았습니다.
새로 만든 것은 맞고 원래 있던 것은 틀린 상태. 목록을 늘릴 때 가장 자주 나오는 모양입니다.
두 번 살려 준 미리보기 기본값
두 스크립트 다 --commit 을 붙이지 않으면 아무것도 안 합니다.
const COMMIT = process.argv.includes('--commit');
…
console.log('\n미리보기만 했습니다. 실제로 올리려면 --commit 을 붙이세요.');주석에 이유가 적혀 있습니다. 「라이브 등록정보를 바꾸는 일이라 실수로 도는 것을 막으려고 그렇게 뒀다.」
이번에 위의 두 문제를 발견한 것이 전부 미리보기 출력에서였습니다. 언어 목록이 아홉 줄 나와야 하는데 여섯 줄이었고, 출시 노트를 세다가 문안을 열어 봤습니다. 기본값이 실행이었다면 둘 다 반영된 뒤에 알았을 겁니다.
바깥으로 나가는 스크립트를 쓸 때 기본값을 「아무것도 안 함」으로 두는 비용은 플래그 하나 더 치는 것입니다. 그 값은 이번 한 번으로 다 뽑았습니다.
등록정보의 소속이 다른 애플과 구글
고치고 나서 순서를 정할 때 알게 된 것이 하나 더 있습니다.
App Store 의 등록정보는 버전에 매달려 있습니다. 그래서 올려 둔 문안은 그 버전 심사에 같이 실려 가고, 심사를 통과해야 사용자에게 보입니다.
Play 의 등록정보는 앱에 매달려 있습니다. 릴리스와 독립이라, 등록정보만 올리면 그것만 따로 검토돼 스토어에 반영됩니다. 아직 옛 버전을 쓰는 사용자도 새 설명을 보게 됩니다.
그래서 Play 는 순서를 생각해야 합니다. 등록정보를 먼저 올리면 「아홉 언어를 지원한다」고 적힌 설명을 보고 받은 앱이 여섯 언어짜리인 구간이 생깁니다. 며칠이면 큰 문제는 아니지만, 알고 고르는 것과 모르고 그렇게 되는 것은 다릅니다. 저는 등록정보와 승격을 붙여서 했습니다.
아직 안 한 것
세 언어의 스크린샷이 없어서 영어 것을 빌려 쓰고 있습니다. 설명은 각 언어인데 화면 속 글자는 영어인 상태입니다. 어색하지만 스크린샷 없이 등록정보만 있는 것보다는 낫다고 보고 넘어갔습니다.
스크립트는 빌린 것을 로그에 밝힙니다.
phoneScreenshots 4장 ← store-assets/1.1.0/android/en/phone (영어 것을 빌림)없는 채로 두면 예외를 던지고 멈추던 자리를, 영어로 대체하고 대신 그 사실을 매번 출력하게 바꿨습니다. 조용히 넘어가면 나중에 왜 영어 스크린샷이 걸려 있는지 아무도 모릅니다.
함께 읽기
- Expo config plugin이 반영되지 않는 이유: prebuild와 mod 순서생명시계 앱에 Face ID 잠금을 붙였습니다. expo-local-authentication 을 설치하고 app.json 에 플러그인과 권한 문구를 적었습니다.
- Android Studio 없이 실기기 빌드: 최소 환경 838MB와 각 구성요소의 역할Expo SDK 57 앱을 안드로이드 실기기에 올리려고 맥을 열었는데, 빌드에 필요한 것이 하나도 없었습니다. java -version은 "Unable to locate a Java Runtime"을 냈고 ANDROIDHOME은 비어 있었으며 adb는 명령어 자체가 없었습니다. Xcode만 들어 있는 기계였습니다.
- Expo 로컬 릴리스 빌드: 17분 만에 죽고 3분 48초에 끝난 이유클라우드 빌드를 떠나 로컬에서 릴리스를 만들어 보기로 했습니다. 서명 자료는 다 모아 두었고 도구도 깔았습니다. 첫 빌드는 17분 51초를 돌고 나서 죽었습니다.
- Expo CNG: android 와 ios 폴더가 리포에 없는 이유Expo 로 만든 앱의 리포를 클론하면 ios/ 도 android/ 도 없습니다. .gitignore 를 열어 보면 아예 막혀 있습니다.
- EAS 없이 Expo 로컬 빌드: 서명 자료를 모으고도 남는 다섯 가지클라우드 빌드를 떠나기로 하고(왜 옮기는지는 따로 적었습니다) 서명 자료부터 모았습니다. iOS 배포 인증서와 프로비저닝 프로파일, 안드로이드 업로드 키스토어와 비밀번호 두 개, 스토어 API 키까지 한자리에 놓고 확인했습니다. 코드 서명 신원도 유효하게 잡혔습니다.