유저가 결제를 했는데 Pro가 안 켜졌습니다. 영수증은 정상이고 서버도 정상이었습니다. 원인은 이름이었습니다.
RevenueCat은 프로젝트를 만들 때 entitlement를 앱 이름 기반으로 자동 생성합니다. 코드는 entitlements["pro"]를 보고 있었죠. 대시보드에 실제로 있던 ID를 전부 뽑아보니 컨벤션이 제각각이었습니다.
cyra Pro / cyra_premium / PulseCalm Pro / GutLog Pro / HQLens Pro /
Innra Pro / Sage Pro / Solca Pro / Zone 2 Longevity Coach Pro / ... / pro"pro"와 일치하는 건 이 중 딱 하나입니다. 나머지는 전부 구매가 성공하고, 영수증이 유효하고, 서버가 200을 주는데, 앱만 계속 free로 판단합니다.
에러가 안 나는 게 진짜 함정이다
entitlements["pro"]는 그냥 nil입니다. 예외도, 로그도, 정상 동작과 구별할 방법도 코드 안에 없습니다. 유저가 문의를 보내야만 알 수 있는 종류의 버그입니다. 대시보드를 만들 때 이름을 안 바꾸면, 그 순간부터 조용히 매출을 흘리기 시작합니다.
전체 앱을 감사하면서 함정이 세 겹으로 나왔습니다.
① ID가 맞아도 안 될 수 있다. 한 앱은 ID가 정확했는데 거기 붙은 제품이 monthly/yearly/lifetime 전부 Test Store 소속이었습니다. ID만 보지 말고 Associated products가 실제 App Store 앱 소속인지 봐야 합니다. 게다가 그 entitlement 이름은 다른 앱의 번들 ID를 달고 있어서, 이름만으로는 어느 앱 것인지도 알 수 없었습니다.
② 한 프로젝트에 entitlement가 여러 개일 수 있다. 위 앱은 방치된 쪽(Test Store 제품만)과 동작하는 쪽(pro, 실 제품)이 둘 다 있었고, 코드가 두 ID를 OR로 보고 있어 결과적으로 정상이었습니다. 하나만 보고 "이 앱은 망가졌다"고 판정하면 틀립니다.
③ 위험군 판정 기준을 처음에 틀렸다 — 이게 제일 중요합니다. 저는 "단일 ID이면서 실행 중 갱신도 없는" 앱만 위험하다고 교집합으로 좁혔습니다. 틀렸습니다. 갱신 유무는 올바른 ID가 얼마나 빨리 반영되는지의 문제일 뿐입니다. ID가 틀리면 아무리 자주 조회해도 영원히 free입니다. 갱신 델리게이트를 제대로 갖춘 앱 두 개가 잘못된 키를 계속 조회할 뻔했습니다. 기준은 하나뿐입니다 — "ID 하나에 전부를 거는가."
대응: 지정 ID 우선, 활성 entitlement 폴백
private extension CustomerInfo {
func hasActiveEntitlement(_ id: String) -> Bool {
if entitlements[id]?.isActive == true { return true }
return !entitlements.active.isEmpty // 대시보드 이름이 뭐든 흡수
}
}Android는 판정 로직을 순수 object로 분리해 단위 테스트를 붙였습니다.
ProEntitlement.isActive(exactIdActive, activeEntitlementIds)적용 범위: 폴백 18개 앱 / 원래 복수 ID OR이라 안전했던 8개 / 다중 티어라 StoreKit2 currentEntitlements 직접 판정 1개.
⚠️ 폴백은 단일 티어 앱에서만 안전합니다. 다중 티어(Basic/Pro)면 "활성 entitlement가 하나라도 있으면 Pro"가 명백히 틀립니다.
중복 entitlement, 지울까?
지우지 않는 쪽이 안전합니다. 삭제는 되돌릴 수 없고 얻는 건 대시보드 가독성뿐입니다. 폴백을 넣은 뒤로는 어느 이름이 활성화되든 앱이 Pro로 인식하므로, 남은 entitlement가 오히려 안전망입니다. 굳이 지운다면 Associated products를 먼저 볼 것 — 실 제품이 하나라도 물려 있고 그게 다른 entitlement에 없으면, 그 제품 구매자는 어디에도 안 걸리게 됩니다. 혼동이 문제라면 삭제 대신 Display Name에 (unused)를 붙이세요.
정직하게 남은 것
- 수정본 18개가 전부 iOS 파일만 건드렸습니다. 계정을 공유하는 앱에서 같은 결제가 플랫폼별로 다르게 동작하는 상태를 하루 동안 만들었습니다. Android는 다음 날 별도 수정했습니다. 커밋이 iOS 경로만 스쳤다는 걸 그때까지 아무도 안 봤습니다.
- iOS 쪽 수정에는 테스트가 없습니다.
CustomerInfoextension이라 테스트하기 어려운 형태로 짰기 때문입니다. Android만 제대로 짠 셈입니다. - 대시보드를 고쳐도 실행 중인 앱은 계속 free입니다. 대부분
configure()1회와 구매/복원 직후에만 refresh합니다. 유저에게 "앱 완전 종료 후 재실행 또는 구매 복원"을 안내해야 합니다.
이 버그의 여파는 매출 분석에도 번졌습니다. "결제됐는데 Pro 안 켜짐" 구간의 이탈을 사용자 이탈로 오해하면 안 되는데, 그 이야기는 노출 1위 앱의 매출은 0원이었다에서 다뤘습니다.
지금 당신 앱의 entitlement 조회 키, 대시보드에 그 이름 그대로 있습니까? 한 글자만 달라도 유저는 결제하고 아무것도 못 받습니다 — 그리고 당신은 로그로도 못 봅니다.