원스토어 콘솔에서는 기본값과 「권장」부터 의심했다

9월 16일, 원스토어 콘솔에 APK를 올리자 서명 옵션이 둘 떴다. 한쪽에 「(권장)」이 붙어 있었다. 그쪽을 고르면 원스토어가 앱을 다시 서명하고, 서명 지문이 바뀌면 카카오와 구글 콘솔에 등록해 둔 키 해시가 전부 어긋난다. 소셜 로그인 넷이 한꺼번에 죽는다

권장이 안 붙은 「앱 서명 사용 안함」을 골랐다. 9월 17일 0시 6분에 검증을 요청했고, 같은 날 오전 9시 54분에 반려 없이 승인됐다. 9시간 48분이다. 콘솔에서 걸린 자리는 서명 옵션 말고도 넷이 더 있었는데, 전부 같은 모양이었다. 화면이 먼저 골라 둔 값이 우리에게 틀린 값이었다

가입부터 상품 생성까지 18분, 패키지명은 두 번 쟀다

9월 2일 오후 2시 44분에 개발자 가입을 마쳤다. 회원구분에 「개인개발자」가 있어 사업자등록 없이 가입됐다. 승인 대기도 없었다. 이메일 인증으로 끝났다. 세션이 고객센터에 물으려고 적어 둔 질문 하나가 여기서 닫혔다

상품을 만들 때 되돌릴 수 없는 칸이 둘이었다. 서비스 구분(「정식 상품으로 등록하기」)과 패키지명이다. 패키지명은 등록 후 영구 고정이다. 그래서 소스와 빌드된 APK 두 출처에서 따로 쟀다

# 소스: android/app/build.gradle.kts 의 applicationId
grep -n 'applicationId' android/app/build.gradle.kts

# 빌드 결과물: APK 안의 매니페스트
aapt2 dump packagename app-release.apk

두 값이 같은 걸 보고 입력했다. 소스만 보면 빌드 플레이버나 접미사가 붙는 경우를 놓친다. 되돌릴 수 없는 값은 만들어지는 쪽과 나가는 쪽을 둘 다 본다

Android Auto 지원 여부와 광고 SDK 여부도 코드로 쟀다. 매니페스트의 car·automotive 선언이 0건, pubspec.yaml의 광고 패키지가 0건이었다. 둘 다 「아니오」다. 원스토어가 수집정보 문서에 이렇게 적어 뒀기 때문이다

수집 정보 항목을 정확하게 선언할 책임은 개발자에게 있으며,
앱의 실제 동작과 선언된 내용 간에 불일치가 발견될 경우
ONE store는 시정 조치를 취할 수 있습니다.

선언을 추측으로 채울 수 없게 만드는 문장이다

상품유형 기본값은 「게임」이었다

공통정보의 기본정보 화면을 열자 상품유형이 이미 「게임」으로 선택돼 있었다. 앱은 시니어 대상 생활·위치 서비스다. 「생활/위치」와 「여행/지역/교통」으로 바꿨다

그대로 뒀으면 반려였다. 더 나쁜 경우도 있다. 검증을 한 번 통과한 뒤 게임과 비게임을 오가면 재검증이 붙는다. 마감 직전에 재검증이 붙으면 일정이 끝난다

이 기본값이 누구에게 맞춰져 있는지는 모른다. 게임 등록이 많은 마켓이라 그럴 수 있다. 다만 새 폼을 열 때마다 이미 채워진 칸이 있는지부터 보게 됐다

수집정보 12항목은 코드로 판정했고, 두 칸이 두 번씩 뒤집혔다

「정보 수집 및 공유 여부」를 「예」로 고르자 12항목이 전부 필수로 펼쳐졌다. 세션이 앱과 서버 소스를 하나씩 뒤져 판정했고, 결과는 예 9, 아니오 3이었다. 그 과정에서 두 칸은 판정이 두 번씩 바뀌었다

전화번호가 먼저였다. 서버 DB에 member.phone VARCHAR(20) UNIQUE와 phone_verified_at 컬럼이 있었다. 검증 시각까지 저장하는 구조다. 세션은 「예」로 적었다. 그런데 앱 코드에서 그 필드를 참조하는 곳은 0건이었고, SMS 어댑터는 Mock 하나뿐이었다. 비상 연락처 저장소에는 이런 주석이 있었다

급할 때 걸 번호 — 기기 로컬에만 둔다
필요한 것은 "인증된 본인 번호"가 아니다

컬럼이 있다는 것과 운영에서 수집한다는 것은 다르다. 체크를 해제했다

이메일은 반대 방향으로 뒤집혔다. 인증 서버의 마이그레이션과 코드에 이메일이 0건이라 세션은 「아니오」로 정정했다. 그런데 이미 게시해 둔 개인정보처리방침이 「비상 알림 이메일」을 적고 있었다. 방침을 단서로 다시 찾으니 안심 설정 화면이 alertEmail을 서버로 보내고 care_status.alert_email에 저장하고 있었다. 다시 「예」가 됐다

grep이 0건을 냈을 때, 찾은 범위가 인증 서버 하나였다는 걸 놓쳤다. 이 판정을 되돌린 건 코드가 아니라 우리가 써 둔 문서였다. 방침과 DB가 서로 어긋나 있다는 것도 이 과정에서 드러났다

외부결제는 되돌릴 수 없는 칸이라 손실의 크기로 골랐다

「외부결제 사용」 칸 앞에서 멈췄다. 나중에 멤버십이나 포인트를 팔 계획이 있을 수 있어서다. 「사용」을 고르면 외부결제 연동 규격에 따른 결제 연동 검증이 검수에 붙는다. OAuth 키 발급, 서버 API, 샌드박스, 상용 PG 연동, 실제 결제와 취소 내역 전송까지다.

결제가 없는 앱이 이 검증을 어떻게 통과하는지는 모른다. 세션은 처음에 「통과할 수 없다」고 적었는데, 그건 모르는 값을 단정한 것이다. 그래서 확률 대신 최악의 크기로 갈랐다

선택최악의 결과
사용안함나중에 결제를 붙일 때 수수료가 달라진다
사용결제 검증에 걸려 9월 21일을 놓치고 출품이 무효가 된다

「사용안함」을 골랐다. 문서의 외부결제 제외 사례에 「무료 쿠폰, 포인트성 캐시」를 통한 구매가 있어, 지금의 포인트 구조와도 맞았다. 확인 팝업에는 인앱상품을 등록하는 시점에 외부결제 변경이 불가능해진다고 적혀 있었다. 변경 불가가 발동하는 시점이 지금이 아니라 인앱상품 등록 때로 읽혔다. 그 해석은 아직 추정이다

저장 실패는 롤백이었다

오후 4시 9분, 외부결제 확인 팝업의 「확인」이 저장 버튼 역할을 했다. 결과는 이랬다

등록 실패. 고객센터에 문의 주시기 바랍니다.

문구만 보면 서버 문제다. 한 번 더 눌렀더니 4시 18분에 진짜 이유가 나왔다

「필수 항목 오류 안내」
상품 정보 > 외부결제 사용
수집정보 세부 유형 > 위치정보 > 신고필증 파일

신고필증이 없어서였다. 그리고 방금 고른 「외부결제 사용안함」이 다시 미입력으로 지목됐다. 저장이 실패하면 그 화면의 값이 전부 날아간다. 이날 채운 값은 세션이 복사용 시트로 옮겨 뒀다. 필증을 받는 데 1주일이 걸렸다(2편)

9월 14일에 필증을 붙이고 다시 저장했다. 이번엔 개인정보취급방침 URL이 빠졌다고 한 번 더 실패했다. 세 번째에 저장됐다. 저장 뒤에는 새로고침해서 상품유형, 필증 파일, 외부결제 값 셋이 서버에 남았는지 확인했다. 저장 버튼이 성공 메시지를 띄웠다는 것만으로는 확인이 아니다

스크린샷은 1MB만 맞추고 「규격 ✅」를 적었다

9월 14일, 부속물을 올리기 직전에 스크린샷 8장의 규격을 다시 봤다. 세션의 기록은 이랬다

규격: 1170×2532 · 각 736KB 이하 (한도 1MB) · 합계 2.7MB   ✅

원스토어 판매정보 문서의 한도는 세 축이다

최소 2개 ~ 최대 8개
1MB 이내인 JPG, PNG
1,300 x 1,300px 사이즈 이하

8장 전부 세로 2,532px로 한도를 넘었다. 1MB 한 축만 재고 ✅를 붙인 것이다. 591×1280으로 다시 만들었고 146~344KB가 나왔다. 원본을 그대로 올렸으면 업로드에서 거부됐다

한도는 항상 여러 축이다. 개수, 용량, 픽셀, 형식을 전부 센 뒤에야 ✅를 적는다

연령등급 설문의 「예」가 이틀 뒤 거짓이 될 뻔했다

같은 날 오후 5시 21분에 IARC 설문을 마쳤다. 다른 마켓에 올린 적이 없어 재사용할 인증 ID가 없었고, 새로 답했다. 세션이 문항별 답 초안을 코드 근거와 함께 만들어 뒀다. 출석 보상의 긁는 복권이 「도박」 문항에 걸리는지까지 검토했다

실제 문항은 예상과 달랐다. 「무작위 보상이 있나」가 아니라 「연령 제한된 상품·행위를 홍보·판매하는 데 중점을 두나」였다. 복권은 걸리지 않았다. 사용자 간 상호작용, 위치 공유, 온라인 콘텐츠를 전부 「예」로 답했는데 결과는 전 지역 전체이용가, 원스토어 3+였다. 차단·신고 기능을 「예」로 답한 게 상쇄한 것으로 보인다. 등급을 낮추려고 거짓으로 적을 이유가 처음부터 없었다

이틀 뒤인 9월 16일, 나는 다른 창에서 신고 기능을 끄라고 했다. 관리자 페이지가 없어 신고를 처리할 수 없었다. 인증서에는 이미 이 문장이 박혀 있었다

사용자 또는 사용자 생성 콘텐츠를 보고할 수 있음

원스토어는 앱 정보가 부정확하면 출시를 중단할 수 있다고 경고한다. 신고 기능을 되살리기로 했다. 그런데 플래그만 켜서는 절반만 돌아왔다

진입점꺼진 방식IARC가 묻는 축
친구 시트 「신고하기」, 대화방 「신고·차단」플래그사용자 신고
게시글 「이 글 신고」「이 사람 신고」, 댓글 신고코드 삭제사용자 생성 콘텐츠 신고

게시글 쪽은 플래그가 아니라 코드로 걷어낸 상태였다. 앱 세션이 게시글 상세 화면의 골든 테스트로 이걸 잡았다. 플래그만 켰을 때는 골든 이미지가 안 바뀌었고, 세 진입점을 되살린 뒤에야 바뀌었다. 친구·대화방만 확인했으면 「신고 살아남」으로 보고 통과시켰을 것이다

0건이 나왔을 때 계측기부터 의심했다

신고 기능을 되살린 빌드로 검증을 요청하기 직전, 세션이 APK 안에서 신고 낱말 넷을 셌다. strings와 UTF-8 grep 모두 0건이었다

그대로 믿었으면 「신고 기능이 빌드에 안 들어갔다」고 보고했을 것이다. IARC 설문을 다시 쓰는 쪽으로 갔을 수도 있다. 세션은 대신 앱에 확실히 있는 낱말인 앱 이름으로 같은 명령을 돌렸다. 그것도 0건이었다. 계측기 문제였다

Dart VM은 문자열을 두 종류로 저장한다. Latin-1 범위면 OneByteString, 그 밖이면 UTF-16 코드 유닛으로 된 TwoByteString이다(Dart SDK runtime/vm/object.h). AOT로 컴파일된 libapp.so 안의 한글은 UTF-16LE 바이트열로 박힌다. UTF-8로 찾으면 없는 것처럼 보인다. 인코딩을 UTF-16LE로 바꾸자 넷이 1·2·2·1건으로 잡혔다

바이트열을 두 인코딩으로 동시에 세는 스크립트는 이렇게 쓸 수 있다

# count_words.py — 사용법: python3 count_words.py libapp.so 신고하기 "이 글 신고"
import sys

data = open(sys.argv[1], "rb").read()
for word in sys.argv[2:]:
    utf8 = data.count(word.encode("utf-8"))
    utf16 = data.count(word.encode("utf-16-le"))
    print(f"{word}\tutf-8={utf8}\tutf-16-le={utf16}")

libapp.so는 APK를 unzip한 lib/arm64-v8a/ 아래에 있다. 이 글을 쓰면서 UTF-16LE로만 한글을 넣은 합성 파일로 다시 돌려 봤다. grep -c와 strings -e l | grep은 둘 다 0건이었고, 이 스크립트의 utf-16-le 열만 잡았다. strings -e l은 16비트 리틀엔디언을 읽지만 출력 가능한 ASCII 범위 문자만 문자열로 인정해서 한글은 걸러진다

같은 이유로, Flutter 빌드에 --dart-define 값이 박혔는지 UTF-8 grep으로 확인하는 검사도 비ASCII 값에는 거짓 안심을 준다. 그 0건은 「안 박혔다」가 아니라 「못 봤다」다. 전부 0건이 나오면 반드시 있는 값으로 계측기를 먼저 시험한다

서명 옵션은 번호가 아니라 문구로 골랐다

바이너리 등록으로 돌아간다. 원스토어 앱 서명 문서는 옵션을 다섯으로 적는다

1. ONE store가 앱 서명키를 관리 보호
2. 이 개발자 계정의 다른 앱과 동일한 키 사용
3. Java Keystore의 키 내보내기 및 업로드
4. 키 내보내기 및 업로드(Java Keystore를 사용하지 않음)
5. ONE store 앱 서명 선택해제

1~3을 고르면 원스토어가 재서명한다. AAB는 1~3만 되고, 한 번 AAB로 전환하면 다시 APK로 판매할 수 없다. 그래서 세션의 준비 문서에는 「APK + 옵션 5」로 적혀 있었다

APK를 고르자 화면에 남은 건 둘이었다. 번호도 없었다

앱 서명 사용 (권장)
앱 서명 사용 안함

「옵션 5번」으로 기억하고 들어갔으면 찾지 못했다. 문구로 골랐다. 「권장」은 원스토어가 키를 관리하는 쪽에 붙어 있다. 키를 잃어버릴 걱정이 없으니 원스토어 입장에서는 맞는 권장이다. 다만 소셜 로그인 키 해시를 이미 등록해 둔 앱에게는 틀린 권장이다

업로드 전에 APK의 서명 인증서 지문을 기준선과 대조했다. 기준선은 9월 1일에 잰 SHA-1(1f3b01d1…bc528e)이다. 확인 명령은 이런 형태다

apksigner verify --print-certs --verbose app-release.apk | grep -E 'SHA-1|v2 scheme'

v2 서명, minSdk 24, SHA-1 일치를 보고 올렸다. 재서명을 안 했는지는 스토어에서 받은 설치본으로 확인됐다. 9월 19일에 기기에서 뽑은 base.apk의 md5가 로컬 출시 APK와 바이트 단위로 같았다. 재서명됐다면 서명 블록이 바뀌어 md5가 달라진다

스토어 설치본으로 소셜 로그인 넷을 신규 계정마다 따로 시험한 기록은 없다. 제출 자료용 화면을 그 설치본에서 로그인한 상태로 찍은 것까지만 확인했다

배포옵션 하나가 마감 직전의 사람 손을 없앴다

마지막은 검증 요청 화면이었다. 공식 안내는 이렇다

앱을 검증하는 데 영업일 기준으로 최대 5일이 소요되며,
예외적인 상황에서는 더 오래 걸릴 수도 있습니다.

요청 시점에 마감까지 2영업일이 남아 있었다. 배포국을 대한민국만으로 둔 것도 이 때문이다. 미국을 넣으면 약 5영업일이 더 붙는다. 배포옵션은 「즉시 적용」, 「직접 적용」, 「예약 적용」 셋이었고 「즉시 적용」을 골랐다. 그러면 「판매대기」를 거치지 않고 승인과 동시에 판매가 시작된다

검증 요청   2026-09-17 00:06:46
승인        2026-09-17 09:54:52   반려 0회 · 즉시 판매중

스토어 링크는 HTTP 200을 돌려줬고, og:description에 우리가 쓴 한 줄 설명 78자가 그대로 들어가 있었다. 「최대 5영업일」은 정말 최대였다. 다만 이건 반려 없이 한 번에 통과한 한 건의 값이다. 다음 앱의 일정을 이 숫자로 잡지는 않는다

콘솔에서 되돌릴 수 없는 칸을 만나면, 이미 선택된 값과 「권장」 표시를 내 앱의 조건으로 다시 읽는다. 기본값과 권장은 마켓의 평균에 맞춰져 있고, 로그인 키 해시나 마감 같은 내 조건은 모른다


시리즈: AI 세션과 앱 출시하기

  1. AI 에이전트가 앱 출시를 대신 못 한 자리는 네 종류뿐이었다
  2. 위치기반 앱은 스토어 저장 버튼에서 사업자등록까지 거슬러 올라간다
  3. 원스토어 콘솔에서는 기본값과 「권장」부터 의심했다 (이 글)
  4. 제출본의 정본은 내 문서가 아니라 콘솔과 업로드된 파일이다

원스토어 화면 문구와 옵션 수는 2026년 9월 기준이다. 연령등급 인증 ID와 스토어 링크는 적지 않았다