StoreKit 2의 환불 감지는 트랜잭션의 단 하나의 속성으로 귀결됩니다. 바로 revocationDate입니다
Apple이 당신의 고객 중 한 명에게 환불하면, 그 환불은 서버 작업이 실행되기 전에 이미 트랜잭션의 revocationDate로 앱 내부에 들어와 있습니다. 이 글은 StoreKit 2의 환불 감지가 기기에서 어디에 나타나는지, revocationDate와 revocationReason이 무엇을 알려주는지, 그리고 왜 클라이언트는 속도를 위한 것이고 서버는 진실을 위한 것인지 설명합니다.

핵심 요약
- StoreKit 2에서 환불된 구매는 그 Transaction에 nil이 아닌 revocationDate를 지니므로, 앱은 서버를 호출하지 않고도 스스로 환불을 감지할 수 있습니다.
- revocationDate는 App Store가 트랜잭션을 환불하거나 고객이 Family Sharing을 통해 그 구매를 잃을 때 설정되므로, nil이 아닌 날짜가 항상 환불을 뜻하지는 않습니다.
- revocationReason은 이유를 알려줍니다. developerIssue는 고객이 당신 앱 안의 문제를 언급했다는 뜻이고, other는 나머지 모든 환불 이유를 포괄합니다.
- Transaction.currentEntitlements는 이미 환불되거나 취소된 구매를 제외하므로, 가장 깔끔한 클라이언트 측 접근 판정은 어떤 제품이 거기에 여전히 나타나는지 여부뿐입니다.
- Transaction.updates는 실행 시작 시점에 수신을 시작한 경우에만 앱이 닫혀 있는 동안 발생한 환불을 전달합니다. 즉 Task가 없으면 환불을 놓칩니다.
- 클라이언트 측 감지는 앱이 열려 있는 동안에만 발동합니다. 그래서 App Store Server Notifications V2의 REFUND가 환불된 사용자에게 서비스를 제공하며 비용을 계속 지불하는 것을 멈추게 하는 권위 있는 신호로 남습니다.
- 환불은 되돌려질 수 있으며, 그럴 때 트랜잭션에서 취소 필드가 제거되므로 당신은 끊었던 접근을 복원해야 합니다.
대부분의 앱은 Apple의 환불을 자사 서버에서, App Store Server Notification을 통해 알게 되며, 같은 환불이 이미 앱 내부에 들어와 있다는 것은 결코 알아차리지 못합니다. 그것은 트랜잭션 위, revocationDate라는 속성 안에 있으며, 이를 읽으면 백엔드 작업을 기다리지 않고도 환불된 고객이 다음에 앱을 여는 순간 그 접근을 끊을 수 있습니다. StoreKit 2의 환불 감지는 대부분의 팀이 건너뛰는 클라이언트 측 신호입니다. 여기서는 환불이 기기에서 정확히 어디에 드러나는지, 무엇을 알려주고 무엇을 알려주지 않는지, 그리고 왜 그것이 서버 알림을 대신하는 것이 아니라 그 곁에 있어야 하는지를 설명합니다.
StoreKit 2 안에서 환불이 나타나는 곳
StoreKit 2는 트랜잭션을 서명된 값으로 건네줍니다. 환불은 트랜잭션을 삭제하지 않습니다. 표시를 남깁니다. Transaction 위의 두 속성이 그 표시를 지니며, 건강한 구매의 전 생애 동안 둘 다 nil로 남습니다. 그중 하나가 nil이 아니게 되면, App Store가 그 구매를 회수한 것입니다.
revocationDate가 뒤집히는 필드입니다
revocationDate는 옵셔널 Date입니다. Apple 자신의 설명은 정확합니다. 그것은 App Store가 그 트랜잭션을 환불했거나 Family Sharing에서 취소한 날짜입니다. 여전히 유효한 구매에서는 nil입니다. 환불이 처리되는 순간, 그것은 그 환불의 타임스탬프를 담습니다. 이 한 가지 확인, revocationDate가 nil이 아닌가, 그것이 클라이언트 측 환불 감지의 전부입니다. 나머지는 모두 그 위에 쌓인 세부 사항일 뿐입니다.
revocationReason은 Apple이 왜 회수했는지 알려줍니다
revocationReason은 날짜 옆에 있으며 그 원인을 설명합니다. StoreKit은 환불에 의미 있는 두 값을 줍니다. developerIssue는 고객이 Apple에, 환불이 당신 앱 안의 실재하거나 인지된 문제 때문이라고 말했다는 뜻입니다. other는 나머지 모든 이유를 포괄합니다. 세 번째 값 upgradedToBundle은 애초에 환불이 아닙니다. 고객이 구독 번들로 옮겨 App Store가 취소한 트랜잭션을 표시합니다. 행동하기 전에 이유를 읽으십시오. 셀 가치가 있는 것은 developerIssue이기 때문입니다. 그것이 무리 지어 나타나는 것은, 당신 자신의 제품이 어디서 망가졌는지를 알려주는 것입니다.
| 속성 | 유형 | nil이 아닌 값이 의미하는 것 |
|---|---|---|
revocationDate | Date? | App Store가 이 날짜에 이 트랜잭션을 환불했거나 Family Sharing을 통해 취소했다 |
revocationReason이 developerIssue | 이유 | 고객이 당신 앱 안의 실재하거나 인지된 문제를 언급했다 |
revocationReason이 other | 이유 | Apple이 항목화하지 않는 다른 어떤 이유로 환불이 일어났다 |
revocationReason이 upgradedToBundle | 이유 | 환불이 아니다. 고객이 구독 번들로 전환하여 트랜잭션이 취소되었다 |
currentEntitlements는 이미 환불된 구매를 떨어뜨립니다
취소 필드를 항상 직접 읽어야 하는 것은 아닙니다. Transaction.currentEntitlements는 고객이 지금 이 순간 여전히 권리를 가진 구매의 시퀀스이며, Apple은 당신이 인정하지 말아야 할 것들을 빼도록 그것을 구성합니다. App Store가 환불했거나 취소한 제품은 거기에 나타나지 않습니다. 만료된 구독도, 다 쓰는 순간 사라지는 소모품도 나타나지 않습니다.
그것이 currentEntitlements를 가장 깔끔한 접근 판정으로 만듭니다. 고객이 무엇을 소유하는지 그것에 물어 정확히 그것을 부여하면, 환불은 단 한 번의 revocationDate 확인도 없이 당신을 위해 권리를 제거합니다. 취소 필드는 당신이 세부, 즉 날짜와 이유를 원할 때, 이벤트를 기록하거나 그에 반응하기 위한 것입니다. 권리 목록은 불을 계속 켜둘지 말지라는 소박한 물음을 위한 것입니다.
실전에서의 StoreKit 2 환불 감지, 실행 시작 시점과 앱 실행 중
앱이 기기에서 환불을 잡을 수 있는 순간은 둘이며, 서로 다른 코드가 필요합니다. 하나는 앱이 열려 있고 환불이 실시간으로, 또는 다른 기기에서 일어날 때입니다. 다른 하나는 실행 시작 시점으로, 닫혀 있는 동안 바뀐 모든 것을 따라잡을 때입니다. 두 번째를 놓치면, 당신의 StoreKit 2 환불 감지는 바로 대부분의 환불이 떨어지는 곳에 구멍을 갖게 됩니다. 고객은 환불을 요청할 때 당신의 앱을 열어두는 일이 드물기 때문입니다.
실행 시작 시점에 수신을 시작하지 않으면 닫혀 있는 동안 일어난 환불을 놓칩니다
Transaction.updates는 시스템이 당신 앱 밖에서 또는 다른 기기에서 트랜잭션을 생성하거나 갱신할 때마다, 환불을 포함해 그 트랜잭션을 방출하는 비동기 시퀀스입니다. Apple의 지시는 단도직입적입니다. 앱이 실행되자마자 그것을 순회하는 Task를 시작하십시오. 그러지 않으면 실행 시작 시점에 단 한 번만 전달되는 트랜잭션을 놓칠 수 있습니다. 밤사이 떨어진 환불은 앱이 다음에 열릴 때 updates를 통해 도착하지만, 그것을 받을 리스너가 이미 실행 중일 때에 한합니다. 리스너가 없으면 이벤트도 없고, 환불은 다른 무언가가 대조할 때까지 보이지 않은 채 남습니다.
같은 기기에서의 구매는 updates를 통해 오지 않습니다
환불을 손으로 테스트하는 사람을 잡는 함정이 하나 있습니다. 같은 기기에서 이루어진 일반 구매는 updates를 통해 도착하지 않습니다. StoreKit은 그것을 구매 호출의 결과에서 곧바로 반환합니다. updates는 대역 외 변화를 위한 것입니다. 환불, Ask to Buy 승인, 오퍼 코드 사용, 그리고 다른 곳에서 이루어진 구매입니다. 그러니 환불 처리는 구매 흐름 주위가 아니라 updates와 currentEntitlements 주위로 구성하십시오. 환불은 판매가 지나온 경로를 되짚어 돌아오는 일이 결코 없기 때문입니다.

클라이언트 측 감지가 당신을 위해 할 수 없는 것
기기에서 환불을 읽는 것은 빠르고 무료이지만, 한계가 있으며, 그것이 없는 척하는 것이 수익이 새는 방식입니다. 기기는 StoreKit이 알려준 것만 알고, StoreKit은 당신의 앱이 실행 중일 때만 말합니다. 환불을 받고 다시는 당신의 앱을 열지 않는 고객은, 당신의 클라이언트 측 확인이 결코 보지 못하는 고객입니다.
revocationDate가 항상 환불을 뜻하지는 않습니다
같은 필드는 Family Sharing에서도 뒤집힙니다. 고객이 공유된 구매에 대한 접근을 잃을 때, 주최자가 그를 제거했거나 공유가 끝나서, 그 트랜잭션도 revocationDate를 얻습니다. 그러니 nil이 아닌 날짜는 고객이 더 이상 이 구매를 갖지 않는다는 뜻이며, 그것이 바로 접근 제어에 필요한 것이지만, 항상 돈이 되돌아 나갔다는 뜻은 아닙니다. 수익을 위해 환불을 세고 있다면, 그 숫자를 믿기 전에 Family Sharing 취소를 진짜 환불에서 분리하십시오.
환불은 되돌려질 수 있습니다
환불이 항상 최종인 것은 아닙니다. Apple은 그것을 되돌릴 수 있으며, 그럴 때 트랜잭션에서 취소 필드가 제거되고 구매는 다시 유효해집니다. 환불에서 접근을 끊었다면, 되돌림에서 그것을 복원해야 합니다. 기기에서는 그것이 깨끗한 트랜잭션을 동반한 또 하나의 updates 이벤트로 나타나고, 서버에서는 별개의 REFUND_REVERSED 알림입니다. 환불만 처리하면, 유효한 영수증을 가진 채 접근이 없는 지불 고객을 방치하게 됩니다.
뒤늦은 취소가 실제로 얼마나 드는가
환불이 단지 판매 가격이 당신의 계정에서 나가는 것에 그치는 일은 드뭅니다. 그것이 정산될 무렵이면 당신은 대개 그 구매를 제공하느라 이미 실제 돈을 썼고, 그 지출은 돌아오지 않습니다. 생성된 이미지는 GPU 분을 소모했습니다. 채팅 응답은 토큰당 청구된 모델 API 호출을 소모했습니다. 업로드는 당신이 지금도 붙잡아 두려고 지불하는 스토리지를 소모했습니다. 그 구매가 크리에이터에게 지급을 대었다면, 그 돈은 이미 나갔습니다. 무엇도 환불로 되돌아오지 않습니다.
클라이언트 측 감지는 당신이 여전히 통제할 수 있는 한 부분, 즉 미래의 지출에 대해 그 창을 좁힙니다. 구매가 환불되었음을 빨리 알수록, 그것을 제공하는 것을 빨리 멈춥니다. 하지만 기기는 앱이 열려 있는 동안에만 알려주므로, 다시는 돌아오지 않는 환불된 사용자는 당신이 서버 측에서 부여한 접근을 그대로 지니며, 백그라운드 작업이나 동기화된 기기가 그를 대신해 움직일 때마다 조용히 당신에게 비용을 치르게 합니다. 클라이언트는 취소를 빠르게 합니다. 그것을 보장하지는 않습니다.
속도를 위해 클라이언트를, 진실을 위해 서버를 쓰라
깔끔한 설계는 두 신호를 각자가 잘하는 일에 씁니다. 기기에서는 Transaction.updates와 currentEntitlements가 환불된 고객이 앱을 여는 순간 즉각적인 로컬 반응을 줍니다. UI를 위해, 그리고 왕복 없이 권리 변경을 마무리하기 위해 적합합니다. 서버에서는 App Store Server Notifications V2가 REFUND 메시지를 보내며, 그것은 앱이 다시 열리든 아니든 도착합니다. 환불된 계정에 백엔드가 돈을 쓰는 것을 확실하게 멈추는 유일한 신호입니다.
| 신호 | 어디에 존재하는가 | 언제 발동하는가 | 무엇을 위해 신뢰하는가 |
|---|---|---|---|
트랜잭션 위의 revocationDate | 기기, StoreKit 2 | 앱이 그 트랜잭션을 읽는다 | 특정 구매가 환불되거나 취소되었음을 알려준다 |
Transaction.updates | 기기, StoreKit 2 | 앱 실행 중에 환불이 떨어지거나, 수신한다면 실행 시작 시점에 | 그 자리에 있는 고객을 위해 즉시 반응한다 |
currentEntitlements | 기기, StoreKit 2 | 고객이 지금 무엇을 소유하는지 확인한다 | 직접 환불을 추적하지 않고 접근을 제어한다 |
REFUND 알림 | 당신의 서버, App Store Server Notifications V2 | Apple이 환불을 처리한다, 앱이 열려 있든 아니든 | 다시는 돌아오지 않는 고객에 대한 서버 측 지출을 멈춘다 |
전화를 들고 있는 고객을 위해 기기 신호를, 자리에 없는 고객을 위해 서버 알림을 연결하십시오. 환불이 두 곳 모두에 나타나는 것은 의도된 것입니다. 그중 하나만 읽는 것이, 환불된 계정이 판매가 이미 사라진 뒤에도 당신에게 계속 비용을 치르게 하는 방식입니다.
자주 묻는 질문
- StoreKit 2에서 환불을 어떻게 감지하나요?
- 트랜잭션의 revocationDate를 확인하십시오. 유효한 구매에서는 nil이고 App Store가 그 트랜잭션을 환불하면 날짜를 담으므로, nil이 아닌 revocationDate가 구매가 환불되거나 취소된 신호입니다.
- revocationDate와 revocationReason의 차이는 무엇인가요?
- revocationDate는 App Store가 구매를 회수한 시점이고, revocationReason은 그 이유입니다. 고객이 당신 앱 안의 문제를 언급했을 때 이유는 developerIssue이고, 그 밖에는 other입니다.
- 환불된 구매가 여전히 currentEntitlements에 나타나나요?
- 아니요. Transaction.currentEntitlements는 App Store가 환불하거나 취소한 구매를 제외하므로, 환불된 제품은 고객의 권리에서 스스로 빠져나가며, 그것이 안전한 접근 판정이 됩니다.
- 앱이 닫혀 있는 동안 일어난 환불을 StoreKit이 내 앱에 알려주나요?
- 실행 시작 시점부터 수신할 때에만 그렇습니다. Transaction.updates는 그 변화를 실행 시작 시점에 한 번 전달하므로, 앱이 실행될 때 그것을 순회하는 Task를 시작해야 하며, 그러지 않으면 다른 무언가가 대조할 때까지 환불을 놓칩니다.
- 클라이언트 측 환불 감지만으로 충분한가요?
- 아니요. 기기는 당신의 앱이 실행 중일 때만 환불을 알게 되므로, 앱을 다시는 열지 않는 고객은 그것에 보이지 않습니다. App Store Server Notifications V2의 REFUND가 어떤 경우에도 당신에게 닿는 신호입니다.
- revocationDate는 항상 고객이 환불받았음을 뜻하나요?
- 아니요. revocationDate는 고객이 Family Sharing을 통해 구매를 잃을 때도 설정되므로, nil이 아닌 날짜는 그들이 더 이상 그 구매를 갖지 않는다는 뜻이지만 항상 돈이 반환되었다는 뜻은 아닙니다.
출처 및 추가 자료
- Apple Developer: Transaction.revocationDate (StoreKit)
- Apple Developer: Transaction.RevocationReason (StoreKit)
- Apple Developer: Transaction.currentEntitlements (StoreKit)
- Apple Developer: Transaction.updates (StoreKit)
- Apple Developer: App Store Server Notifications V2 notificationType (REFUND, REFUND_REVERSED)
- Apple Developer Tech Talk: Support customers with StoreKit 2 and App Store Server API
RefundHalt
App Store와 Google Play 환불 자동 처리
계속 읽기
App Store와 Google Play의 모든 환불 기한은 카운트다운이며, 각각 몇 시간을 주는지 알려드립니다
App Store와 Google Play의 모든 환불은 시계를 작동시키며, 대부분은 당신 없이 흘러갑니다. Apple의 가장 짧은 환불 기한은 12시간, Google의 지불 거절 기한은 24시간이며, 2026년 8월 3일부터는 놓친 지불 거절 기한이 단순한 판매 손실이 아니라 청구서가 됩니다. 당신의 계정에 닿는 모든 기한을 여기서 정리합니다.
무효화된 구매 알림을 사용하면 환불이 도착하는 순간 Google Play 서버가 접근 권한을 취소할 수 있습니다
Google Play는 구매가 환불, 차지백, 또는 무효화되는 즉시 무효화된 구매 알림을 서버로 푸시할 수 있습니다. 이 알림은 purchaseToken, orderId, productType, refundType를 담고 있으며, 의미하는 바는 단 하나, 접근 권한을 취소하라는 것입니다. 이를 읽고 연결하는 방법을 소개합니다.