개----빡센 앱스토어 구독 넣기

빌드 넘버가 버전 하나만에 20이 올랐어요..

Jun Noh

주주 1.4.6 버전이 플레이스토어에 올라간 게 7월 1일이었으니까.. 구독 결제 로직을 붙이고, 본격적으로 광고를 붙이는 작업을 한 게 지난 주 월요일이었다.

월요일에 빡세게 시작을 해서, 겨우겨우 화요일 밤에 끝내고 자정 정도에 플레이스토어와 앱스토어에 새 빌드를 제출했다.

플레이스토어는 워낙에 테스트 환경도 좋았고, 결제 로직에 있어서 조금 관대한 편이라 금방 끝났는데

문제는 앱스토어였다.

수요일 첫번째 리젝을 시작으로 약 20번의 테스트 빌드와 6번 정도 추가 리젝을 끝으로 이 글을 쓰고 있는 일요일 저녁인 지금. 드디어 심사가 통과되었다. (그 동안 밀린 패치가 많아서.. 바로 1.4.7이 올라갈 예정이다.)

그래서 오늘은 이 미친 거절 러쉬의 원인과 어떻게 해결했는 지를 좀 쓰려고 한다. (1.4.6의 첫 빌드가 38이었는데… 지금 56입니다..ㅋㅋㅋㅋ)

초기 세팅 — 뭐부터 준비해야 하나

거절 러쉬 얘기 하기 전에 — IAP 붙일 때 뭘 세팅해야 하는지부터 좀 정리하고 가야한다. 이걸 해놔야 뒤에 나올 삽질 얘기가 이해가 된다.

App Store Connect

계약이랑 세금, 금융. 이게 다 되어있어야 IAP 가 아예 작동을 한다.

  • 유료 앱 계약 서명 + 활성화
  • 은행 계좌 등록 (나는 KB국민은행, KRW 수령 / USD 로열티로 셋팅)
  • 세금 양식: 대한민국, 미국(W-8BEN), 브라질, 멕시코 — 국가별로 다 제출해야 한다
  • 규정 쪽은 DSA 랑 한국 전자상거래법

구독 상품 등록은 이렇게 했다.

  • 그룹 만들기: “주주 프로”
  • 상품 만들기: zuzoo_pro (Product ID: zuzoo)
  • 가격: ₩1,900 / 월
  • 인트로 오퍼: 첫 달 무료 (1개월 free trial)
  • 지역: 175개 전체 다
  • 로컬라이제이션: 한국어 + 영어
  • 심사용 스크린샷 + 심사 노트

앱 정보 쪽에서는 아래 정도만 챙기면 된다.

  • Bundle ID: com.zuzoo.mobile
  • Age Rating (나는 처음에 “광고=아니오” 로 잘못 설정해서 나중에 수정했다)
  • 이용약관 + 개인정보처리방침을 앱 내 링크로 노출

코드 쪽

모바일react-native-iap v15 를 붙였다.

import { useIAP } from 'react-native-iap';

const {
  connected,
  subscriptions,
  fetchProducts,
  requestPurchase,
  restorePurchases,
  finishTransaction,
} = useIAP();
  • StoreKit 2 콜백이 안 오는 케이스를 대비해서 getAvailablePurchases 로 수동 폴링도 걸어뒀다 (2s → 5s → 10s)
  • Product ID 랑 Reference Name 이 다를 수 있는데, 코드에서는 Product ID (zuzoo) 로 fetch 해야 한다. 이거 헷갈리면 뒤에서 시간 왕창 날림

여기까지가 당연히 해야 할 기본기. 이제부터 삽질 얘기.

문제 1 — 브라질 세금 양식

세팅 다 했다고 생각하고 sandbox 에서 상품 fetch 를 돌렸는데 — 아예 안 잡힌다.

“SKU not found” 만 뜨는데, sandbox 에서 이런 게 뜨면 진짜 어디부터 뜯어봐야 할지 답이 없다.

포럼을 한참 뒤지다가 App Store Connect 의 비즈니스 → 계약, 세금 및 금융 섹션에 “조치 필요” 배지가 붙어있는 걸 찾았다.

브라질 세금 양식이 미제출 상태였고 — 그러니까 Paid Apps Agreement 가 fully active 가 아니었던 거다.

이 상태에서는 StoreKit 이 상품 자체를 non-purchasable 로 취급한다더라. 다른 나라 세팅이 다 되어 있어도 하나만 빠지면 이렇게 된다는 거.

브라질 세금 양식 제출 → 전체 계약 활성화 → sandbox fetch 정상.

여기까지는 그래도 뭐… 내 실수니까, 쉽게 찾을 수 있었다.

문제 2 — SKU not found, 근데 리뷰팀 기기에서만

여기서부터 진짜 미쳤다.

증상이 이랬다.

  • 내 TestFlight 기기 → 결제 정상
  • 리뷰팀 sandbox → 결제 시도하자마자 “결제 실패 · sku-not-found” alert
  • 리젝 → 재제출 → 또 sku-not-found 리젝. 반복.

내 기기에서는 되는데 리뷰팀에서만 안 되는 상황이 진짜 답이 없다. 재현이 안 되니까.

시도했던 방법들

이건 뭐 내가 어찔 할 수 있는 문제가 아니니, 처음에는 “sandbox 는 됩니다, 캐시 지워달라” 요청했다.

결과는 당연하게 통하지 않았다. (그냥 기도메타긴 했음 이건)

Apple Support 에 IAP orphan 리셋 티켓도 넣어봤고 — 답장 오는 데 이틀 걸렸는데 결국 도움 안 됐다.

IAP 메타데이터 강제 변경 → 상태 재시도 도 해봤는데 이것도 아니었다.

목요일 저녁 즈음에는 진짜 뭘 해야 할 지를 몰라서 멍하니 앉아있었다.

결정적 단서는 스크린샷에

금요일 아침에 리뷰팀 회신을 다시 봤는데, 스크린샷이 첨부되어 있었다. 별 기대 없이 열어봤는데, 결제 화면에 ₩1,900 / 월 로 표시되고 있었다.

아무렇지 않게 지나치려다가, 생각해보니 앱스토어 심사 테스터가 한국 리전에 있을리가 없었다.

즉, $0.99 가 떴어야 했는데, 원화로 가격이 나오고 있었다.

₩1,900 은 코드에 하드코딩된 PRO_PRICE_FALLBACK 문자열이었다. 즉:

StoreKit 이 product 를 fetch 못 한 상태에서 결제 버튼이 눌러진 거다.

진짜 원인 — race condition

t=0        connected: true          ← 버튼 즉시 활성화 (guard 없음)
t=?        subscriptions 채워짐     ← fetchProducts 응답 (500ms ~ 2000ms)
  • 내 기기: StoreKit 캐시 hit → fetchProducts 즉시 응답 → 항상 정상으로 보였음
  • 리뷰팀 (첫 설치): 캐시 miss → fetch 지연 → 그 사이 버튼 탭 → unresolved product 로 requestPurchase → sku-not-found

이걸 겨우 찾는 데 4일 걸렸다.

역시, 제일 어려운 버그는 재현이 불가능한 버그다…

Fix (build 55 즈음)

<Pressable
  disabled={!connected || !product || purchasing}
  onPress={handleSubscribe}
>
  {!connected
    ? '스토어 연결 중...'
    : !product
      ? '가격 정보 확인 중...'
      : '첫 달 무료로 시작하기'}
</Pressable>

주요 변경은 아래 정도.

  • 버튼 disabled 조건에 !product 추가 → product 못 잡히면 절대 안 눌리게
  • 로딩 상태 시각화 넣기: “스토어 연결 중…” → “가격 정보 확인 중…” → 정상 CTA
  • fetchProducts 에 exponential backoff 재시도 (5회, 총 ~7.5s)
  • 진단 로그 추가 (productFound: false 여부 확인용)

이거 붙이고 나서야 리뷰팀도 결제 시트까지 정상 도달했다. sku-not-found 는 이걸로 해소됐다.

문제 3 — Guideline 3.1.2(c), 첫 달 무료가 너무 크게 표시됨 (반나절)

결제 통과 되고 “이제 진짜 끝났다” 싶었는데 몇 시간 뒤에 새 리젝 사유가 왔다.

“The auto-renewable subscription promotes the free trial more clearly and conspicuously than the billed amount.”

자동 갱신 구독의 무료 체험이 실제 결제 금액보다 눈에 띄게 강조됐다, 는 지적.

원인 UI (before)

가격은 24pt 얇은 텍스트인데, “첫 달 무료” 는 노란 pill 배경까지 붙어있어서 시각적으로 얘가 제일 두드러졌다. Apple 정책상 이러면 위반이랜다..

좀, 넘어갑시다…ㅋㅋㅋ 왤케 빡빡해요…

암튼 이건 큰 문제는 아니니까, “첫 달 무료, but 다음달부터 얼마!” 를 강조했다.

이러고 나서 build 56 제출. 그리고 방금 심사 통과 알림이 왔다.

Apple 리뷰 정책은 UI 시각 우선순위까지 본다.

이건 진짜 겪어봐야 안다.

문구만 맞으면 되는 게 아니라 폰트 크기, 색상, 배경 강조까지 다 보는 거 같다.

정리

  1. 모든 나라의 세금 양식은 미리 다 제출해두자 → Paid Apps Agreement 가 fully active 여야 IAP 가 산다
  2. 결제 버튼 guard 에 !product 반드시 포함 → connected 만 보면 race condition 터진다
  3. UI 에서 가격이 free trial 보다 시각적으로 커야 한다 → 폰트 크기/색/배경 전부 리뷰 대상
  4. 하드코딩 fallback 가격은 위험 → fetch 실패 시 그 값으로 결제 시도되면 sku-not-found 원인이 됨
  5. 리뷰팀 리젝 스크린샷은 반드시 확대해서 본다 → 힌트가 거기 다 있다

마치며

이번에 진짜 얻은 교훈은 — TestFlight 통과 = 리뷰 통과가 아니라는 것.

내 기기, 내 계정, 내 캐시로는 절대 재현 안 되는 문제가 리뷰팀에서 터진다.

그래도 build 56 이 통과됐으니, 이제 실제 유저 결제 붙이면 또 어디서 뭐가 터질지 모르겠고…

지금부터는 웹훅 붙이는 게 다음 순서다.

마침.

다른 글 보기