RSS듀오랩스
모바일 앱

Play 스토어 등록정보 자동화: EAS가 덮지 않는 한 칸

작성자
듀오랩스 대표·8분 읽기

앱에 언어 셋을 더하고 스토어에 올리려던 참이었습니다. iOS 쪽은 명령 하나로 끝났습니다.

npx eas-cli metadata:push

store.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  (영어 것을 빌림)

없는 채로 두면 예외를 던지고 멈추던 자리를, 영어로 대체하고 대신 그 사실을 매번 출력하게 바꿨습니다. 조용히 넘어가면 나중에 왜 영어 스크린샷이 걸려 있는지 아무도 모릅니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.