App Store 리젝 대응 순서: 재제출보다 답변이 먼저인 이유
심사 결과 메일이 한 통 오고, App Store Connect의 상태가 REJECTED로 바뀝니다. 이때 가장 먼저 하고 싶어지는 일은 뭔가를 고쳐서 다시 올리는 것입니다. 저는 그게 대개 가장 비싼 선택이라고 생각합니다. 고칠 곳을 모르는 채로 고치면 심사는 처음부터 다시 시작되고, 다음 결과가 올 때까지 또 며칠이 지나갑니다.
재제출은 추측을 검증하는 가장 느린 방법
리젝 메일은 대부분 보일러플레이트입니다. 같은 문면이 여러 상황에 쓰이기 때문에, 메일만 읽고 원인을 확정할 수 있는 경우는 생각보다 적습니다. 그 상태에서 재제출하면 「우리가 짐작한 원인」을 하나 검증하는 데 며칠을 쓰는 셈입니다. 틀렸으면 같은 판정이 다시 오고, 그때는 반복 제출 이력까지 남습니다.
해결 센터(Resolution Center)의 답변은 다릅니다. 빌드 번호를 소모하지 않고, 심사 대기열을 다시 타지도 않습니다. 저희 앱 하나는 답변서 한 통으로 끝났습니다. 리젝을 받은 날부터 승인까지 13일이 걸렸는데, 그중 12일은 답을 쓰지 않고 있던 기간이었습니다. 보내고 나서는 이틀이었습니다. 그 숫자가 이 글에서 제일 중요합니다. 심사가 오래 걸린 게 아니라 저희가 멈춰 있었습니다.
화면이 아니라 API로 읽어야 하는 사실들
대응문을 쓰기 전에 사실을 세워야 하는데, 이 단계에서 틀리기 쉽습니다. 저는 제출일을 잘못 읽은 적이 있습니다. App Store Connect 화면에 보이는 날짜를 제출일로 알고 문서에 적었는데, 그건 버전을 만든 날이었고 실제 제출은 일주일 뒤였습니다. 대응 문서에 「리젝이 열린 채로 다음 앱을 올렸다」는 잘못된 반성문을 쓸 뻔했습니다.
App Store Connect API로 읽으면 화면을 해석할 필요가 없습니다. 버전 상태는 /v1/appStoreVersions, 실제 제출 시각은 /v1/reviewSubmissions, 심사 대상으로 붙어 있는 빌드는 같은 버전의 build 관계에 들어 있습니다. 인증은 App Store Connect API 키(.p8)로 ES256 JWT를 만들어 Authorization 헤더에 넣으면 되고, Apple ID 로그인이나 2단계 인증이 필요 없습니다.
여기서 한 번 더 놀란 적이 있습니다. 콘솔에서 심사 대상 빌드를 새 빌드로 바꿨는데, 저장이 되지 않아 옛 빌드가 그대로 붙어 있었습니다. 화면에서는 고른 것처럼 보였고, API로 읽으니 이전 빌드 번호가 나왔습니다. 결국 PATCH /v1/appStoreVersions/{id}/relationships/build로 바꾸고 다시 읽어서 확인했습니다. 올렸다는 것과 올라갔다는 것은 다릅니다.
답변으로 풀리는 리젝과 고쳐야 풀리는 리젝
모든 리젝이 답변으로 풀리지는 않습니다. 앱을 고쳐야만 끝나는 것들이 있고, 그건 빨리 인정하는 편이 낫습니다. 자주 만나는 것들을 정리하면 이렇습니다.
| 조항 | 어떤 앱이 걸리나 | 먼저 확인할 것 | 답변으로 풀리나 |
|---|---|---|---|
| 2.1 완성도 | 심사 중 크래시, 빈 화면, 동작하지 않는 기능 | 릴리스 빌드에서 재현되는지. 에러 경계가 있는지 | 재현이 안 되면 답변으로, 재현되면 수정 |
| 2.3 메타데이터 불일치 | 스크린샷이 실제 화면과 다르거나 설명이 없는 기능을 말함 | 스크린샷과 현재 빌드의 화면을 나란히 대조 | 대개 메타데이터만 고치면 끝 |
| 3.1.1 결제 | 디지털 재화를 외부 결제로 팔 때 | 결제 흐름이 앱 안에 있는지 | 수정이 필요 |
| 4.2 최소 기능 | 웹뷰 래퍼, 정보만 나열한 앱 | 앱에서만 되는 일이 무엇인지 한 문장으로 | 기능이 있으면 답변으로, 없으면 수정 |
| 4.3 스팸 | 같은 계정에 비슷해 보이는 앱이 여럿일 때 | 코드와 자료를 공유하는지, 스토어 자산이 서로 닮았는지 | 차이가 실재하면 답변으로 |
| 5.1.1 권한 | 목적 문자열이 기본값이거나 쓰지 않는 권한이 들어감 | 빌드된 바이너리의 *UsageDescription 목록 |
수정이 필요 |
| 5.1.1(v) 계정 삭제 | 계정을 만드는 앱에 삭제 경로가 없음 | 앱 안에서 삭제까지 가는 경로 | 수정이 필요 |
표에서 오른쪽 칸이 「수정이 필요」인 줄은 답변을 오래 써도 달라지지 않습니다. 반대로 「답변으로」인 줄에서 먼저 빌드를 고치면, 고칠 필요가 없던 것을 고치면서 시간을 쓴 셈이 됩니다.
답변서에 넣는 것, 리뷰어가 할 수 있는 일
답변이 통했던 이유를 저는 구체성이라고 봅니다. 「저희 앱은 독창적입니다, 재고해 주십시오」는 읽는 사람에게 할 일을 주지 않습니다. 할 일이 없으면 원래 판정을 유지하는 쪽이 가장 싸기 때문에, 그대로 유지됩니다. 그래서 어느 탭의 어느 화면에서 무엇을 확인할 수 있는지를 화면 이름과 함께 적습니다. 리뷰어가 앱을 열어 두세 번 두드리면 끝나는 확인이어야 합니다. 답변서를 어떻게 구성했는지는 앞 글에 자세히 적었습니다.
넣지 않은 것도 중요합니다. 아직 하지 않은 일을 했다고 쓰지 않습니다. 초안에는 「스크린샷을 바꿨고 심사 노트를 영어로 제공했다」가 들어 있었는데 둘 다 계획만 있는 상태였습니다. 열어 보면 바로 드러나고, 그 손해가 원래 리젝보다 큽니다. 지웠습니다.
기다리는 동안 바꿔도 되는 것
답변을 보내고 나면 기다리게 되는데, 그동안 손을 놓고 있을 필요는 없습니다. 메타데이터 수정은 재제출이 아닙니다. 설명, 키워드, 스크린샷, 심사 노트는 빌드와 별개로 올라가고 빌드 번호를 소모하지 않습니다.
그중에 비용이 0에 가까운 것이 하나 있습니다. 심사 노트의 언어입니다. 앱 UI가 한국어 전용이면 스토어 설명은 한국어로 두는 게 맞습니다. 영문 설명만 붙이면 영어 사용자가 영어 앱으로 알고 받고, 그건 다른 종류의 리젝을 만듭니다. 하지만 심사 노트는 리뷰어가 읽는 유일한 설명문이고, 그게 한국어로만 적혀 있으면 위에서 말한 「탭 두세 번」 안내가 전달되지 않습니다.
다른 스토어도 따로 굴러갑니다. Google Play 심사는 Apple 심사와 무관하게 진행되므로, 한쪽이 막혀 있다고 다른 쪽을 멈출 이유는 없습니다.
대응보다 싼 제출 전 30분
리젝을 겪고 나서 알게 된 것은, 대응보다 예방이 훨씬 싸다는 평범한 사실이었습니다. 제출 전에 확인할 것은 많지 않습니다. 빌드된 바이너리에서 권한 목적 문자열을 직접 읽고(설정 파일이 아니라 바이너리입니다), 릴리스 빌드로 한 바퀴 돌아 크래시가 없는지 보고, 스토어 스크린샷이 지금 빌드의 화면과 같은지 확인하고, 앱에서 데이터를 지우는 경로가 실제로 있는지 눌러 봅니다.
특히 스크린샷은 낡기 쉽습니다. 앱을 고치면 코드만 낡는 게 아니라 스토어에 박아 둔 그림도 같이 낡습니다. 저희는 앱 안의 숫자와 출처 표기를 고친 뒤에 스크린샷 여덟 장을 다시 찍었습니다. 찍지 않았다면 스토어에는 고치기 전의 주장이 그대로 남아 있었을 겁니다.
리젝을 받은 날의 순서
리젝을 받으면 다음 순서로 움직이시면 됩니다. 상태를 API로 읽어 사실을 세우고, 사유가 답변으로 풀리는 쪽인지 고쳐야 하는 쪽인지 표에서 가늠하고, 답변이면 같은 날 쓰고, 수정이면 무엇을 고칠지부터 정합니다. 그리고 답을 받기 전에는 재제출하지 않습니다.
이 중에서 가장 쉽게 어기는 것은 첫 줄이 아니라 「같은 날」입니다. 메일을 받은 날은 대응문을 쓰기 가장 싫은 날이고, 하루를 미루면 다음 하루는 더 쉽게 미뤄집니다. 심사 대기열은 그동안 줄어들지 않습니다.
함께 읽기
- App Store 4.3 리젝 대응: 리뷰어가 탭 세 번으로 확인할 수 있는 답변서성경 족보 1.0 을 8월 27일에 냈고, 그날 리젝됐습니다. Guideline 4.3, 스팸입니다. 다른 개발자가 제출한 앱과 바이너리·메타데이터·컨셉이 유사하고 차이가 미미하다는 문면이었습니다.
- Expo config plugin이 반영되지 않는 이유: prebuild와 mod 순서생명시계 앱에 Face ID 잠금을 붙였습니다. expo-local-authentication 을 설치하고 app.json 에 플러그인과 권한 문구를 적었습니다.
- App Store Connect 업로드: Xcode 로그인 없이 API 키로아카이브는 만들어졌습니다. 이제 IPA 로 뽑아 올리기만 하면 되는데, 여기서 Xcode 에 애플 계정을 붙이라는 요구가 세 번 나옵니다. 세 번 다 피할 수 있습니다.
- WWDR 중간 인증서: 코드 서명 신원이 0개로 나오는 이유$ security import dist-cert.p12 -k /Library/Keychains/login.keychain-db
- Play 스토어 등록정보 자동화: EAS가 덮지 않는 한 칸앱에 언어 셋을 더하고 스토어에 올리려던 참이었습니다. iOS 쪽은 명령 하나로 끝났습니다.