모든 글
Stock Calculator · 4분 읽기

첫 앱 출시기 4 — Play Store에 올리기 - 서명, 등록정보, 콘솔에 안 가는 법

업로드 키와 Play 앱 서명, 되돌릴 수 없는 versionCode, targetSdk 마감, 필수 고지들. 그리고 Gradle 명령 한 줄로 올리기까지 — 두 번 막혔던 권한 오류와, 스크린샷 55장이 통째로 빠졌던 이야기입니다.

#android#play-store#출시기#gradle#개인개발

지난 글에서는 AdMob 세 자리와 동의(UMP), 그리고 인앱 결제를 붙였어요. 테스트 ID로 출시하거나 실제 ID로 개발하는 사고를 빌드에 맡겨 막는 이야기까지 했죠.

이제 앱은 다 됐습니다. 올리기만 하면 되는데, 그 “만”이 이틀이었어요. 이번 마지막 편에서는 그 이틀에 무엇이 있었는지를 순서대로 적어볼게요. 서명부터 시작합니다.

서명 — 키를 잃어버리면 앱을 잃습니다

가장 먼저 정할 것이 서명 방식이에요. 그리고 Play 앱 서명을 쓰는 게 맞습니다.

옛날 방식은 내가 만든 키로 서명해서 올리고, 그 키를 평생 지키는 것이었어요. 잃어버리면 그 앱을 영원히 업데이트할 수 없습니다. 새 패키지명으로 새로 내야 해요.

Play 앱 서명은 최종 서명키를 구글이 보관합니다. 내가 만드는 건 업로드 키 — “올릴 자격이 있다”만 증명하는 키예요. 잃어버려도 파국은 아닙니다. 콘솔에서 재설정을 요청하면 돼요(며칠 걸립니다).

빌드 설정은 이렇게 해뒀습니다.

val keystoreProperties = rootProject.file("keystore.properties")
if (keystoreProperties.exists()) {
    val props = Properties().apply { keystoreProperties.inputStream().use { load(it) } }
    signingConfigs {
        create("release") {
            storeFile = rootProject.file(props.getProperty("storeFile"))
            storePassword = props.getProperty("storePassword")

        }
    }
}

키와 비밀번호는 git 밖에 둡니다. 파일이 없는 기계(CI 등)에서는 서명 없이 빌드돼요. 의도한 동작입니다. 비밀번호를 저장소에 넣는 것보다 CI에서 서명을 못 하는 편이 훨씬 나으니까요.

그리고 키를 만들자마자 다른 곳에 백업해두세요. 기기 한 대에만 있는 상태로 두면, 그 기기가 죽는 순간 며칠짜리 재설정 요청부터 다시 시작해야 합니다.

versionCode는 되돌릴 수 없어요

두 번호의 성격이 완전히 다릅니다.

무엇되돌릴 수 있나
versionCode콘솔이 세는 정수아니오. 한 번 본 번호는 다시 안 받습니다
versionName사람에게 보이는 글자네. 순서도 안 따집니다

이걸 모르고 시험 삼아 번들 하나를 올렸다가 versionCode = 1을 태워 먹었어요. 그 뒤로는 2부터 시작해야 했습니다.

// versionCode 1은 예전 번들이 Play 콘솔에 먹었습니다 — 콘솔은 한 번 본 코드를 다시 받지 않아요
versionCode = 6
versionName = "1.0.0"

반대로 versionName은 자유롭습니다. 테스트 트랙에 세 번 올리면서 1.0 → 1.3까지 갔는데, 그동안 세상에 나간 건 없었어요. 공개 출시가 “1.3.0 데뷔”가 되는 건 사실과 다르니까 첫 공개판을 1.0.0으로 되돌렸습니다. versionCode만 계속 올라가요.

targetSdk에는 마감이 있습니다

이건 몰랐으면 출시 직전에 막혔을 일이에요.

Play는 신규 앱과 업데이트에 최신 API 타게팅을 요구하고, 그 마감이 매년 있습니다. 이 앱을 만들던 시점의 마감은 2026-08-31, 요구 수준은 API 36이었어요.

Chore: targetSdk 를 36 으로 올린다 — Play 마감 2026-08-31

targetSdk를 올리면 동작이 바뀌는 것들이 있어요(권한, 백그라운드 제한 등). 그래서 올린 뒤 한 번 다 돌려봐야 합니다. 출시 전날 할 일이 아니에요.

코드 말고도 준비할 게 많아요

콘솔이 요구하는 것 중 코드가 아닌 것들입니다.

개인정보 처리방침 URL — 웹에 실제로 떠 있어야 해요. “아무것도 수집하지 않는다”도 적어서 올려야 합니다. 안 적으면 없는 것으로 안 봐줘요.

데이터 보안 양식 — 무엇을 수집하고 어디로 보내는지 콘솔에서 선언합니다. 광고 SDK가 들어가면 “없음”이 아니게 돼요. 여기서 거짓을 적으면 나중에 앱이 내려갑니다.

앱 콘텐츠 설문 — 광고 포함 여부, 타겟 연령층, 콘텐츠 등급.

그리고 저희 경우엔 필수 고지가 둘 있었어요.

  • 오픈소스 라이선스 — 이 앱은 Pretendard 글꼴을 씁니다. SIL OFL 1.1이라 고지 없이 배포하면 위반이에요. 설정에 라이선스 화면을 넣었습니다.
  • 투자 면책 — 금융 계산 앱이니 “계산 도구이지 투자 자문이 아니다”를 밝힙니다.

이 둘은 설정 화면에서 있으면 좋은 것이 아니라 의무라서, 지금은 테스트가 지키고 있어요.

등록정보와 스크린샷

제목, 짧은 설명, 긴 설명, 스크린샷, 아이콘 512×512, 피처 그래픽 1024×500. 언어를 늘렸으니 이걸 로케일마다 준비했습니다. 11개 언어 × 스크린샷 5장이에요.

스크린샷은 손으로 찍지 않고 에뮬레이터에서 자동으로 뽑았어요. 좌표를 못 박으면 언어마다 글자 길이가 달라 버튼 위치가 밀리거든요. 그래서 uiautomator로 요소를 찾아서 누르게 했습니다. 그래야 독일어 화면에서도 같은 자리가 찍혀요.

콘솔에 안 가고 올리기

여기서 삶이 편해졌어요. Gradle Play Publisher 플러그인을 쓰면 명령 한 줄로 올라갑니다.

./gradlew publishReleaseBundle                       # 내부 테스트
./gradlew publishReleaseBundle --track production     # 프로덕션
./gradlew publishReleaseListing                       # 등록정보·스크린샷

준비물은 서비스 계정 키 하나예요(Google Cloud에서 만들어 콘솔에 권한을 줍니다). 그리고 여기서 두 번 막혔습니다.

accessNotConfigured — Cloud 프로젝트에서 Google Play Android Developer API를 켜야 해요. 무료고, 켜는 데 1분입니다. 안 켜져 있다는 걸 아는 데 30분이 걸렸어요.

The caller does not have permission — Play 콘솔의 “사용자 및 권한”에서 서비스 계정에 앱 접근 + 출시 권한을 줘야 합니다. Cloud 쪽 IAM 역할을 아무리 만져도 안 돼요. 권한은 Play 콘솔 쪽에 있습니다.

둘 다 에러 메시지가 “키가 잘못됐다”처럼 읽혀서 키를 몇 번이나 다시 만들었어요. PERMISSION_DENIED가 뜨면 키가 아니라 위 두 곳을 보세요.

그리고 스크린샷 55장이 통째로 빠져 있었어요

이 글을 쓰다가 발견한 건데, 시리즈에 넣을 만한 종류라 적어둡니다.

11개 로케일 등록정보를 만들어 넣었는데 폴더 위치가 틀렸어요. 플러그인이 읽는 자리는 app/src/main/play/인데 app/play/에 만들었더라고요.

그래서 실제로 올라가는 건 옛 한국어 등록정보 하나뿐이었습니다. 스크린샷 3장에, 옛 문구로요.

이런 종류가 제일 나쁩니다. 빌드도 성공하고 업로드도 성공하거든요. 무엇이 빠졌는지는 스토어를 열어봐야 알고, 그때는 이미 올라간 뒤예요.

올리기 전에 플러그인이 무엇을 모았는지 먼저 보는 습관을 들이는 게 훨씬 쌉니다.

./gradlew generateReleasePlayResources
# → app/build/generated/gpp/release/play/ 아래에 실제로 올라갈 것이 모입니다

트랙 — 한 번에 전체 공개로 가지 않습니다

Play에는 트랙이 여러 개 있어요.

  • 내부 테스트 — 최대 100명, 심사 거의 없이 바로. 개발 중엔 여기만 씁니다
  • 비공개 테스트 — 지정한 테스터
  • 공개 테스트 — 누구나 참여
  • 프로덕션 — 전체 공개

기본 트랙을 내부 테스트로 박아뒀어요.

play {
    track.set("internal")
    defaultToAppBundles.set(true)
    releaseStatus.set(ReleaseStatus.COMPLETED)
}

--track production을 명시해야만 프로덕션으로 갑니다. 실수로 전체 공개되는 것보다 매번 한 단어 더 치는 게 나아요.

고치고 나서 적어 둔 것

솔직히 적으면, 이 시리즈를 쓰는 지금 앱은 아직 테스트 트랙에 있어요. 프로덕션 승격은 안 했습니다.

그런데도 이 네 편을 쓴 이유는, “빈손에서 스토어 콘솔까지” 사이에 있는 것들이 검색으로 잘 안 나왔기 때문이에요. 대부분의 글이 코드만 다루거나 콘솔 스크린샷만 붙여놓는데, 실제로 제 시간을 잡아먹은 건 그 사이의 것들이었습니다.

어떤 키를 먼저 만들어야 하는지, 어떤 번호가 되돌릴 수 없는지, 어떤 에러 메시지가 사실은 다른 곳을 가리키는지 같은 것들이요.

2주 동안 배운 걸 한 줄로 줄이면 이렇습니다.

만드는 데 열흘, 올리는 데 이틀. 그런데 그 이틀이 열흘만큼 깁니다.

다음 글은 실제로 프로덕션에 올린 뒤에 쓸게요. 그전까지는 앱 안쪽에서 배운 것들을 안드로이드를 만들며 배운 것 시리즈에 적어두고 있습니다.