첫 출시용 출시 노트를 다 써 두고 Play Console 을 열었더니, 프로덕션에는 이미 1.7 이 나가 있었다.
「오늘 누가 쏘냐?」는 커피값 낼 사람을 미니 게임으로 정하는 Unity 파티게임이다. Play 스토어 비공개 테스트에 올리기까지는 개발기 연재에 적었고, 이번 연재는 그다음, 정식 출시 이야기다.
저장소의 현재 상태 메모에는 "1.6 으로 비공개 테스트 중, 14일을 채우면 프로덕션 액세스 신청"이라고 적혀 있었다. 그래서 1.8 을 빌드하면서 첫 출시 준비를 통째로 했다. 앱 소개형 출시 노트, 신청 설문 답변 초안, 프로덕션 트랙 설정까지. 그런데 그 단계는 이미 지나 있었다.
기록은 1.6 에 멈춰 있었다
개인 개발자 계정은 비공개 테스트를 테스터 12명 이상으로 14일 연속 유지해야 「프로덕션 액세스 신청」이 열린다. 테스터는 옵트인 링크를 열어 설치해야 집계되고, 중간에 12명 아래로 떨어지면 날짜를 다시 센다.
신청 설문은 테스터를 어떻게 모았는지, 무엇을 바꿨는지, 출시 준비가 됐다고 어떻게 판단했는지를 묻는다. 초안의 원칙은 하나였다. 심사자는 답변을 테스트 기록과 대조하니 확인한 사실만 쓴다. 게임이 다섯 개에서 열다섯 개로 늘어난 것, 분담 방식과 정산 화면이 들어간 것은 저장소 기록 그대로 적었다. 테스터 수나 받은 의견처럼 기억에만 있는 부분은 괄호로 비워 두고, 짐작으로 채운 예시에는 "실제 의견으로 바꾸거나 지울 것"을 붙였다. 그럴듯하게 지어낸 의견 하나가 거절 사유가 될 수 있다.
문제는 전제였다. 콘솔의 프로덕션 트랙에는 1.7(버전 코드 11)이 이미 공개돼 있었다. 신청도 첫 심사도 끝난 뒤였다. 저장소 메모가 콘솔을 따라가지 못한 거다. 출시 상태의 정답은 콘솔에 있다. 출시 문서는 첫 출시 절차에서 프로덕션 업데이트 순서로 다시 썼다.
첫 출시가 업데이트로 바뀌면
전제가 바뀌자 고칠 게 나왔다.
먼저 출시 노트. 이 앱은 버전마다 A(직전 버전에서 올라오는 사람용)와 B(한 버전 이상 건너뛴 사람용) 두 벌을 쓴다. 1.8 에는 처음 설치하는 프로덕션 사용자를 위한 앱 소개형 C 를 더 써 뒀었다.
하지만 프로덕션에 1.7 이 있으면 1.8 을 받는 사람은 전부 1.7 에서 올라온다. C 는 지웠고, 비공개 테스트와 프로덕션 모두 A 를 쓰기로 했다.
<ko-KR>
초성 퀴즈를 말로 맞힐 수 있어요!
• 「말하기」를 누르고 정답을 외치면 알아듣고 바로 판정해요
• 엉뚱하게 알아들으면 「땡!」과 함께 들은 말이 떠요
• 마이크 권한은 처음 누를 때만 물어요. 허용하지 않아도 자판으로 그대로 할 수 있어요
음성 인식은 폰의 음성 인식 기능을 써서 인터넷이 필요할 수 있어요.
</ko-KR>
규약이 바뀐 버전에는 "모든 폰이 최신 버전이어야 함께할 수 있어요"를 반드시 넣는데, 1.8 은 규약이 1.7 과 같아 뺐다. 대신 마이크 권한 줄을 넣었다. 처음 「말하기」를 누르면 권한 창이 뜨고, 거부해도 게임은 된다는 걸 미리 알려 두고 싶었다. 큰 화면 회전은 태블릿 사용자만 느끼는 변화라 넣지 않았다.
앱 콘텐츠 선언도 다시 봤다. 데이터 보안 답은 "수집 안 함" 그대로다. 소리는 폰의 음성 인식 서비스가 글자로 바꾸고, 앱은 인식된 글자만 받아 그 문제의 정답 판정에 쓰고 저장하지 않는다. 같이 하기 중에는 자판으로 친 답과 똑같이 방장 폰으로만 보낸다. 대신 엉뚱한 곳 하나를 찾았다. 「광고」 선언이 "광고 있음"이었다. 광고 SDK 는 없으니 "없음"으로 바로잡았다. 대상 연령도 만 13세 이상으로 정리하고 처리방침을 거기에 맞췄다.

12 를 버리고 13 으로
1.8 AAB 는 처음에 버전 코드 12 로 빌드했다. 빌드 함수가 AAB 를 만들 때마다 코드를 하나 올리고 프로젝트 설정에 저장하기 때문이다.
// ---- 버전 코드: 스토어 빌드마다 +1 (Play 는 올라가는 값만 받는다) ----
if (aab) PlayerSettings.Android.bundleVersionCode += 1;
int code = PlayerSettings.Android.bundleVersionCode;
그런데 1.7 의 출시 전 보고서에 권장 조치 두 개가 남아 있었다. 큰 화면에서 크기 조절과 방향 제한을 풀라는 것, R8 로 코드를 줄이라는 것. 둘 다 오류는 아니라 무시해도 출시는 된다. 하지만 1.8 이 프로덕션으로 나가는 첫 업데이트라면 같은 경고를 달고 나갈 이유가 없었다. 방향 고정을 풀되 폰은 실행 중에 세로로 다시 묶고, 릴리스 빌드에 R8 을 켰다. 그러고 다시 빌드하니 코드 13 이 나왔다.
여기서 잠깐 고민했다. 코드 12 는 Play 에 한 번도 올리지 않았으니 번호를 되돌려 12 로 다시 빌드해도 받아 준다. 그래도 되돌리지 않았다. 되돌리려면 무조건 하나씩 올리는 빌드 함수에 예외를 둬야 하고, 그러면 같은 번호가 서로 다른 두 파일을 가리키게 된다. 빌드 폴더에는 이미 권장 조치가 빠진 WhoPays-1.8-12.aab 와 그 디버그 기호 파일이 있다. 같은 이름에 다른 내용이 덮이면, 나중에 비정상 종료 스택을 해석할 때 어느 기호 파일이 맞는지 알 수 없다.
Play 가 요구하는 건 "앞 번호보다 크다"뿐이고 번호가 이어질 필요는 없다. 12 는 비워 두고, 출시 문서에 "코드 12 AAB 는 권장 조치 전 빌드라 올리지 않는다"를 굵게 적었다. R8 매핑 파일은 AAB 안에 같이 들어가니 따로 올릴 필요가 없었다.
처리방침이 먼저다
1.8 출시 순서의 맨 위에는 앱이 아니라 웹 페이지가 있었다. 운영 중인 개인정보 처리방침에는 "마이크 권한은 사용하지 않습니다"라고 적혀 있었다. 1.8 은 마이크를 쓴다. 심사자가 앱의 권한과 처리방침을 나란히 놓고 보면 둘이 어긋난다. 그래서 저장소의 새 판(마이크와 음성 입력 항목, 대상 연령 항목을 넣은 판)을 운영 서버에 먼저 올리고, 그다음에 「검토 시작」을 누르기로 했다.
처리방침은 WebGL 빌드와 같은 사이트, whopays.addsoft.co.kr 에서 나간다. 파일 하나지만 서버에 들어가 손으로 덮어쓰면 무엇을 올렸는지, 그 전 운영본이 무엇이었는지가 남지 않는다. 그래서 Web Deploy 로 게시하는 PowerShell 스크립트를 만들었다. -PrivacyOnly 면 처리방침 한 파일, 아니면 WebGL 빌드 결과 전체를 올린다.
스크립트가 하는 일은 대부분 확인이다. 지금 브랜치와 커밋, 커밋하지 않은 변경을 보여 주고, 배포 계정으로 읽기 전용 접속을 해 본다. 계정은 저장소에 두지 않고 실행할 때마다 환경변수에서 읽으며, 출력에 비밀번호가 보이면 별표로 가린다. 그다음 운영본과 저장소본의 시행일을 나란히 찍는다.
# 운영 사이트의 파일을 UTF-8 로 읽는다 (PS 5.1 은 charset 이 없으면 한글을 깨뜨리므로 바이트로 받아 직접 푼다)
function Get-ProdText([string]$path) {
$r = Invoke-WebRequest -UseBasicParsing -Uri ("{0}/{1}?t={2}" -f $siteUrl, $path, [DateTime]::Now.Ticks)
return [Text.Encoding]::UTF8.GetString($r.RawContentStream.ToArray())
}
function Get-EffectiveDate([string]$html) { if ($html -match '시행일:\s*(\d{4}-\d{2}-\d{2})') { return $Matches[1] } }
function Normalize([string]$s) { ($s -replace "`r`n", "`n").Trim() }
Windows PowerShell 5.1 은 응답에 charset 이 없으면 한글을 다른 인코딩으로 푼다. 그러면 "시행일" 글자부터 깨져 정규식이 아무것도 못 찾는다. 그래서 바이트로 받아 UTF-8 로 직접 푼다. 주소 끝의 시각 값은 캐시가 예전 판을 돌려주지 않게 붙였고, 비교 전에는 줄바꿈과 앞뒤 공백을 맞춘다. 그렇지 않으면 같은 문서도 다르다고 나온다.
둘이 이미 같으면 "올릴 것이 없습니다"로 끝난다. 다르면 확인을 묻고, 답을 받을 수 없는 환경이면 게시하지 않고 멈춘다. 확인 없이 올리려면 -Yes 를 명시해야 한다.
덮어쓰기 전에 받아 두기
확인을 통과하면 바로 올리지 않고 운영본부터 받아 둔다. 처리방침만 올릴 때는 그 파일 하나를, 전체를 올릴 때는 사이트 전체를 시각별 백업 폴더에 내려받는다. 백업이 실패하면 게시하지 않는다. 되돌릴 길이 없는 게시는 하지 않는다는 규칙이다.
$msArgs = @('-verb:sync', "-source:contentPath=$Source", "-dest:contentPath=$site,$remote", '-enableRule:DoNotDeleteRule')
if (-not $IncludeWebConfig) { $msArgs += '-skip:objectName=filePath,absolutePath=\\web\.config$' }
DoNotDeleteRule 은 서버에만 있는 파일을 지우지 않게 한다. sync 는 대상을 원본과 똑같이 만들기 때문에 빌드 폴더에 없는 파일은 서버에서 지워 버린다. 그리고 web.config 는 기본으로 건너뛴다. 빌드는 wasm 과 압축 파일의 MIME 을 등록한 web.config 를 같이 만들지만, 운영 서버에는 운영 서버의 설정이 있다. 정말 바꿔야 할 때만 -IncludeWebConfig 를 붙인다.
게시가 끝나면 운영 주소에서 처리방침을 다시 받아 저장소본과 대조하고, 전체 게시였다면 index.html 응답까지 본다. 문제가 있으면 백업 경로를 알려 주고 실패 코드로 끝난다. msdeploy 가 성공이라고 해도 사용자가 보는 주소에서 확인되기 전에는 성공이 아니다. 1.8 의 처리방침도 이 스크립트로 올리고 대조까지 마쳤다.
첫 의견이 할 일 목록으로
1.8 이 공개되고 첫 의견은 리뷰가 아니라 Play 스토어 게임 페이지에 새로 생긴 Q&A 로 들어왔다. 스토어 앱에서만 보이는 곳이라 의견 보는 곳을 따로 적어 뒀다.
의견은 두 가지였다. 하나는 참가자 이름 바꾸기가 불편하다는 것. 들여다보니 결함이었다. 이름 칸을 눌러도 기본 이름이 남아 한 글자씩 지워야 했고, Android 에서는 자판 위에 입력창이 따로 떠서 게임 화면의 이름 칸에는 커서가 보이지 않았다. 고칠 방향도 적었다. 기본 이름이면 누를 때 비우고, 빈 채로 끝내면 기본 이름으로 되돌린다. 같은 입력칸 생성 함수를 쓰는 칸은 한 곳에서 고친다.
다른 하나는 캐릭터를 늘리고, 누를 때마다 넘기는 대신 골라서 바꾸고 싶다는 것. 새 기능인데 함정이 있다. 같이 하기에서는 방장이 참가자의 캐릭터 번호를 보낸다. 캐릭터를 아홉 번째로 늘리면 1.8 게스트 폰은 그 번호를 그릴 수 없다. 규약 버전을 올려 구버전을 막을지, 구버전에 보낼 때만 번호를 범위 안으로 바꿀지 정해야 한다. 할 일 목록에 그 경고를 같이 적었다. 의견만 옮겨 적으면 나중에 이걸 잊는다.
목록에는 1.8 에서 미뤄 둔 것도 모았다. 「말하기」 첫 사용 때 권한 창보다 먼저 "소리는 저장하지 않아요"라고 설명하기. 같이 하기에서 외친 정답을 옆 폰이 알아듣는 문제는 재미로 두고 지켜보기. 큰 화면 회전을 태블릿 실기기로 확인하기. 공개 뒤 Android vitals 에서 음성 입력이나 R8 탓의 비정상 종료 살피기. 새 버전 작업은 이 목록을 펼쳐 놓고 순서를 정하는 데서 시작한다.
정식 출시는 생각보다 조용했다. 버튼 하나 누르는 순간이 아니라, 기록이 콘솔을 따라잡고, 처리방침이 앱을 앞서고, 쓰지 않을 번호 하나를 비워 두는 일이었다. 다음 편은 1.8 에 넣은 Play 권장 조치 두 개, 큰 화면 회전과 R8 이야기다. 그 뒤로 스토어 등록 정보에 무엇을 적었는지, 초성 퀴즈 음성 입력을 어떻게 만들었는지가 이어진다.