지난 계정 이야기에서 개발자 계정 만드는 게 생각보다 무거웠다고 했어요. 그런데 계정은 시작일 뿐이더라고요.
앱은 다 만들었어요. 산책 트래킹도, 백그라운드 경로 기록도, 걸음 수도 다 붙였어요. 이제 출시 버튼만 누르면 되는 줄 알았는데, 그 버튼 앞에 관문이 줄줄이 서 있었어요.
덜 달라고 했는데
먼저 권한을 줄였어요. 산책 경로를 화면이 꺼진 채로도 기록해야 하는데, 그러려고 iOS의 “항상 위치 허용”(NSLocationAlwaysAndWhenInUseUsageDescription) 같은 무거운 권한을 달라고 하면 스토어 심사가 까다로워지거든요. 그래서 산책하는 동안에만 위치를 쓰는 “액티브 세션 트래커” 구성으로 바꿨어요. 스트라바나 나이키 런 같은 앱들이 쓰는 방식이에요.
Always 권한 요청을 코드에서 뺐으니, 같은 정리라고 생각하고 Info.plist의 Always purpose string도 지웠어요.
덜 요구하니까 더 쉬울 줄 알았어요. 그런데 아니었어요.
ITMS-90683 — 안 쓰는데 설명은 하라니
빌드를 다 만들고 Transporter로 올리는데 거부당했어요.
ITMS-90683: Missing purpose string in Info.plist
Apple의 업로드 정적분석은 런타임 호출 여부를 보지 않아요. 링크된 바이너리가 그 API 심볼을 참조하기만 해도 대응하는 purpose string을 요구해요. 우리가 Always를 요청하지 않아도, 쓰고 있는 geolocator(13.0.4) 바이너리가 requestAlwaysAuthorization을 참조하고 있으면 그걸로 끝이에요.
안 쓰는데 설명은 하라니. 뺐던 문구를 도로 넣었어요.
키를 되살려도 런타임 프롬프트는 여전히 WhenInUse만 떠요. 프롬프트를 만드는 건 요청 코드지 plist 키가 아니거든요. 즉 “Always 요청 제거”와 “Always 문자열 유지”는 완전히 별개의 결정인데, 저는 그 둘을 한 덩어리로 묶어서 정리했다가 걸린 거예요.
터지는 지점도 얄궂어요. 심사가 아니라 업로드 단계예요. 빌드를 다 만들고 나서 거부되고, 빌드번호는 이미 소비돼서 +1로 재빌드해야 해요(저는 4에서 5로 올렸어요).
그래서 지금은 업로드 전에 산출물을 먼저 열어봐요. 소스 Info.plist에 있다는 건 증거가 아니고, 실제로 IPA 안에 들어간 것만 증거니까요.
unzip -o -q build/ios/ipa/<앱>.ipa -d /tmp/ipa-check
plutil -p /tmp/ipa-check/Payload/*.app/Info.plist \
| grep -i -e UsageDescription -e CFBundleVersion -e CFBundleShortVersionString
교훈은 하나예요. 플러그인을 추가하는 건 그 플러그인이 참조하는 모든 권한의 purpose string을 추가하는 것과 같아요. 판단 기준은 “내가 쓰는가”가 아니라 “바이너리가 참조하는가”예요. 나중에 사진 첨부로 image_picker를 넣을 땐 이 패턴을 그대로 적용해서, 카메라·마이크를 실제로 안 쓰는데도 NSCameraUsageDescription·NSMicrophoneUsageDescription을 선제로 넣어뒀어요. 그랬더니 그건 한 번에 통과했고요.
거절은 한 번이 아니었다
purpose string만 문제였던 게 아니에요.
이미 출시된 버전이라 같은 버전 이름으로는 새 빌드를 못 올린다더라고요. 버전을 올렸어요. 그랬더니 이번엔 안드로이드에서 그 버전 코드는 이미 썼다고 했어요. 또 올렸어요. iOS 빌드번호와 Android 버전 코드가 각자 따로 카운트되는데, 거절당할 때마다 한 칸씩 소모돼요.
그러고 나서도 App Store Connect에서 “이번 버전의 새로운 기능”이 비어 있다고 또 막혔어요. 하나를 넘으면 다음 관문이 기다리고 있었어요.
기계 말고 사람이 막혔다
코드랑 빌드는 오히려 순순히 됐어요. 정작 막힌 건 손에 쥔 것들이었어요.
안드로이드는 백그라운드 위치 권한을 선언하면 그 기능을 실제로 쓰는 화면을 영상으로 제출해야 해요. 폰을 연결했는데 아무리 해도 컴퓨터가 폰을 못 잡았어요. 알고 보니 충전만 되는 케이블이었어요. 새로 산 폰엔 화면 녹화 기능이 아예 없어서 앱을 따로 깔았고요. 녹화하려고 보니 백그라운드 알림엔 내가 정해둔 문구 대신 앱 이름만 덩그러니 떠 있었어요.
만드는 것보다, 그걸 눈앞에서 굴려 보이는 게 더 손이 갔어요.
통과라는 의식
그래도 결국 양쪽 다 통과됐어요.
앱을 다 만들면 끝인 줄 알았는데, 통과라는 게 또 하나의 관문이었어요. 지난번 계정 만들 때 느꼈던 그 무게가, 출시 앞에서 한 번 더 반복됐어요.
작은 거절들을 하나씩 넘고 나니, 이것도 일종의 의식 같았어요. 가볍게 내보낼 수 있는 게 아니라는 걸, 관문마다 확인시켜 주는.