스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
StoreKit 테스팅의 새로운 기능
앱 내 구입 및 구독을 테스트할 수 있는 최신 도구를 확인하세요. 여러분의 제품을 App Store Connect에서 Xcode의 StoreKit 테스팅으로 가져오는 방법을 보여드리고, 거래 관리자의 향상된 기능에 대해 배우며, Xcode Previews(미리보기)에서 앱 내 구입 흐름에 대해 살펴보겠습니다. 또한 샌드박스 환경에서 Apple ID를 설정할 경우에 대한 모범 사례를 안내하고, 환불 요청, 가격 인상 동의, 청구 재시도 등을 위한 테스트를 만드는 방법을 보여드립니다.
리소스
- Auto-renewable subscriptions overview
- Handling Subscriptions Billing
- Implementing offer codes in your app
- Learn more about setting up offer codes
- Reducing Involuntary Subscriber Churn
- Setting up StoreKit Testing in Xcode
- Testing In-App Purchases with sandbox
관련 비디오
WWDC23
WWDC22
WWDC21
WWDC20
-
다운로드
♪ ♪ 안녕하세요 저는 Greg입니다 'StoreKit 테스트의 새로운 기능' 영상에 오신 걸 환영합니다 이 세션에서는 피터와 제가 StoreKit 앱 내 구입 테스트의 새로운 기능에 관해 말씀드리죠 우선 Xcode 14를 이용하여 앱 내 구입 테스트를 간소화하는 방법을 알아보고 새로운 기능을 활용하여 앱 내 구독을 적용할 때 코너 케이스까지 처리하는 방법을 살펴본 뒤 마지막으로 피터가 샌드박스 테스트 환경의 개선 사항을 보여드릴 겁니다 도넛을 파는 푸드 트럭 상인에게 강력한 기능을 제공하는 앱 '푸드 트럭'을 통해 살펴보죠 이미 StoreKit를 통합하여 판매 이력 기능이 있는 '푸드 트럭' 전체 버전을 제안할 수 있게 했고 '소셜 피드' 서비스의 개선 버전을 구독할 수 있게 했습니다 세션에서는 Xcode의 StoreKit 테스트를 사용하여 앱 내 구입 기능을 테스트할 겁니다 WWDC 2020에서 Xcode의 StoreKit 테스트를 도입하여 Xcode에서 직접 앱 내 구입을 테스트할 수 있게 됐죠 올해는 Xcode 14로 StoreKit 앱 테스트 생명 주기의 업데이트 사항을 공유할 수 있어 기대됩니다 이전처럼 Xcode에서 StoreKit 설정 파일을 만들고 적용한 앱 내 구입 테스트를 시작할 수 있으며 App Store Connect에 앱을 설정하지 않아도 되죠 앱을 App Store Connect에서 설정할 준비가 되면 Xcode 14에 완전히 새로운 기능을 추가하여 App Store Connect에 입력한 것과 똑같은 앱 내 구입 제품을 Xcode StoreKit 테스트에서 사용할 수 있게 됐습니다 이미 스토어에 앱이 있다면 Xcode StoreKit 테스트를 바로 사용할 수 있으며 StoreKit 설정 파일을 새로 만들 필요가 없죠 이 기능을 통해 앱 내 구입을 한 번만 설정하면 같은 설정을 로컬의 Xcode와 유닛 테스트, 샌드박스 환경에서 사용할 수 있고 출시 때 App Store에서 사용할 수 있습니다 Xcode로 App Store Connect의 제품을 쉽게 동기화할 수 있죠 먼저 App Store Connect에서 '소셜 피드 '와 같은 구독 상품을 설정한다고 합시다 그리고 Xcode에서 동기화된 설정 파일을 만들면 Xcode에 제품 데이터를 로딩하죠 만약 미국 영어 제목을 수정하고 싶다면 App Store Connect에서 정보를 수정하고 다시 Xcode에서 설정을 동기화합니다 동기화한 설정을 변환하여 언제나 편집할 수 있는 로컬 파일로 만들 수도 있죠 동기화된 설정을 로컬 설정으로 변환하는 건 한 방향으로만 가능한 작업이며 다시 동기화하려면 새 설정 파일을 만들어야 합니다 이미 '소셜 피드 '라는 구독 그룹을 설정했는데 푸드 트럭 앱에서 제공하는 '소셜 피드'의 개선된 버전이죠 이제 Xcode로 넘어가서 Xcode의 StoreKit 테스트로 이런 제품을 사용하는 법을 알아봅시다 저는 Mac에 푸드 트럭 프로젝트를 열어 놨죠 먼저 StoreKit 설정 파일을 생성하기 위해 파일 메뉴로 가서 새 파일을 만들고 StoreKit로 필터하고 다음을 클릭합니다
Xcode 14에서는 설정 파일을 새로 만들면 App Store Connect와 파일을 동기화할 수 있는 체크 박스가 있죠 로컬 파일을 만들려면 이름을 입력하고 체크하지 마세요 동기화를 설정하려면 체크 박스를 선택하고 올바른 팀과 앱이 선택됐는지 확인합니다 필요하면 선택 메뉴를 이용하여 다른 팀과 앱을 선택할 수 있죠 다음을 클릭하고 파일 저장 위치를 선택합니다 파일을 저장하자마자 앱 내 구입 메타데이터가 App Store Connect와 동기화하죠 데이터를 다운로드하면 계속 앱 작업을 하며 활동 바에서 진척 상황을 확인할 수 있습니다 동기화가 끝나면 전형적인 StoreKit 설정 파일과 모습이 다르다는 걸 눈치채셨을 거예요 동기화된 파일이 읽기 전용 상태기 때문이죠 Xcode의 데이터를 전부 볼 수 있지만 수정하려면 App Store Connect를 열어야 합니다
소셜 피드 월간 상품을 Safari에 띄웠죠 연간 제품과 차별화하기 위해 이 제품의 영어 제목에 접미사를 추가할게요
수정이 끝났으니 저장하고 Xcode로 돌아가죠
설정 파일에 수정 사항을 반영하려면 왼쪽 아래 있는 동기화 버튼만 누르면 됩니다
동기화가 끝나면 Xcode에 변동 사항이 반영돼 있죠
동기화된 파일은 읽기 전용이지만 로컬 파일로 데이터를 복사하여 Xcode에서 빠르게 수정할 수 있습니다
설정 파일에서 항목을 복사하는 것에 더하여 동기화된 파일을 편집 가능한 로컬 파일로 변환할 수도 있죠 일단 동기화된 파일을 열고 에디터 메뉴로 가서 '로컬 StoreKit 설정으로 변환하기'만 클릭하면 됩니다
파일을 변환하면 되돌릴 수 없다는 걸 유의하십시오 다시 앱과 동기화하려면 새 StoreKit 설정 파일을 만드세요 전 App Store Connect와 이 파일을 동기화하고 싶으니 취소하고 나가겠습니다 이제 파일을 동기화했으니 테스트 환경을 설정하죠 시작하려면 계획 에디터를 열고
실행 동작을 선택하고 환경 설정을 선택합니다
환경 설정에서 피커 메뉴로 StoreKit 환경을 바꿀 수 있죠 '없음'을 선택하면 샌드박스로 연결하고 푸드 트럭을 선택하면 Xcode 환경으로 연결합니다 현재 테스트 요구 사항에 따라 환경을 이렇게 쉽게 바꿀 수 있으며 두 환경 모두 같은 제품과 구독 메타데이터를 사용하죠 이제 동기화된 설정 파일을 선택합시다
Xcode에 StoreKit를 설정했으니 테스트로 넘어가죠 SwiftUI 앱을 사용 중이어서 Xcode에서 구독 상점을 미리 볼 수 있습니다
Xcode 14부터 StoreKit 설정 파일의 제품이 바로 SwiftUI 미리 보기에 나타나죠 실제 앱 내 구입 데이터로 멋진 상점 UI를 만들고 테스트하기가 훨씬 쉬워졌습니다 이제 제품에 설명을 추가하여 제품 선택지에 상세 정보를 포함시킬게요 제품의 현지화된 설명을 담은 텍스트 뷰를 추가하죠
App Store Connect에 설정한 제품 설명이 미리 보기에서 바로 업데이트됩니다 이제 훨씬 낫군요
UI가 멋진 형태가 됐으니 iPhone에서 앱을 실행하고 기능 테스트를 시작하죠
Xcode 14의 StoreKit 거래 관리자에 강력한 도구가 추가됐습니다 앱을 실행하면서 거래 관리자를 열려면 디버그 바의 구매 아이콘을 누르세요
오른쪽에 나타나는 거래 인스펙터를 통해 거래에 관련된 세부 사항을 볼 수 있습니다 앱 내 구입의 상태를 이해할 수 있는 도구죠 예를 들어, 소셜 피드 의 구독 날짜가 만료된 것과 도래하는 갱신 정보를 볼 수 있습니다 또한 제품과 구독 집단 구독 프로모션에 관한 설정 파일로 넘어갈 수 있죠 구독 그룹 옆에 있는 넘어가기 버튼을 클릭하면
설정 파일의 소셜 피드 로 이동합니다
이 인스펙터는 더 상급 테스트 사례를 볼 때 도움이 될 거예요
거래를 필터할 수도 있어 소셜 피드 의 갱신과 관련된 거래 목록을 탐색하기도 좋죠 연간 판매 이력 기능에도 접근할 수 있습니다
구독 갱신 목록이 너무 많아서 어떤 거래로 이 기능을 잠금 해제했는지 알기 힘들죠 해당 거래를 쉽게 찾으려면 먼저 제품의 ID를 입력하고
자동 완성 메뉴에서 제품 ID 필터를 선택하세요
구입 날짜로 필터할 수도 있어서 현재 진행 중인 구입만 볼 수도 있습니다
소셜 피드 의 구독이 만료됐으니 앱으로 들어가서 다시 구독하죠
이제 구독을 시작했으니 새로운 거래가 나타나는 걸 볼 수 있습니다
Xcode에서 앱 내 구입 테스트를 개선하는 방법을 살펴봤는데 App Store Connect의 제품과 구독을 동기화하고 SwiftUI 미리 보기로 StoreKit 설정을 사용하고 거래 관리자의 새 도구를 활용하는 방법도 알아봤습니다 이제 Xcode의 새로운 기능으로 푸드 트럭의 앱 내 구입 기능을 계속 테스트하고 상급 구독 사례를 다루죠 먼저 환불 요청 테스트부터 보겠습니다 사용자가 푸드 트럭 구입에 대한 환불을 요청하는 기능이죠 다음은 프로모션 코드를 테스트하여 소셜 피드 구독자에게 프로모션을 제안하고 푸드 트럭의 UI를 통해 가격 인상을 알아보겠습니다 마지막으로, 소셜 피드 의 비자발적 이탈을 감소시키는 청구 재시도와 유예 기간을 살펴보죠 환불 요청을 테스트하려면 앱의 지원 화면으로 이동하여 최근 환불 거래를 선택할 수 있습니다 이를 위한 코드는 간단하죠 뷰에 refundRequestSheet 뷰 제어자를 추가하여 환불 버튼을 눌렀을 때 isPresented 바인딩을 true로 바꿉니다 이제 실제로 확인하죠
Binding이 true일 때 요청 시트가 뷰 위로 나타납니다 Xcode 환경에서 테스트할 때 우리가 선택한 케이스는 StoreKit API RevocationReason과 일대일 대응되죠 '개발자 문제'를 선택하고 '환불 요청'을 누르세요
App Store에서는 환불 요청을 처리하는 시간이 걸리지만 Xcode 또는 샌드박스에서 테스트할 때는 환불 요청하면 거래가 즉시 환불되죠 거래 관리자에서 갱신된 거래에 관한 인스펙터에서 우리가 선택한 취소 사유와 취소 날짜를 볼 수 있습니다
거래 관리자에서 환불 버튼을 클릭하여 환불을 테스트할 수도 있죠 환불 요청 API는 푸드 트럭을 이용하는 고객에게 훌륭한 지원을 제공합니다 Xcode에서 환불 요청을 테스트하는 법을 살펴봤으니 StoreKit를 사용하여 환불 거래 처리 방법을 알아보죠
거래를 환불한 후에는 Transaction.updates 시퀀스에서 갱신된 거래 값을 내보냅니다 revocationDate와 revocationReason 속성으로 환불된 거래를 감지할 수 있죠 두 가지 취소 사례를 쉽게 테스트하려면 Xcode의 환불 요청 시트에서 그에 해당하는 선택지를 고르세요 Xcode에서 환불 요청 시트를 테스트하는 방법이었습니다 Xcode나 샌드박스 환경을 사용할 때 iOS와 macOS에서 작동하죠 Xcode로 테스트하려면 iPhone이나 iPad가 필요하며 iOS나 iPadOS 15.2 이후 버전에서 실행하세요 Mac에서 Xcode를 테스트하려면 macOS 12.1 이후 버전이 필요하죠 이제 구독 프로모션 코드를 테스트해 봅시다 로컬 StoreKit 설정 파일을 이용할 거예요 새로운 프로모션 코드를 만들려면 구독을 선택하고 프로모션 코드 테이블에서 '플러스'를 누르세요 그러면 프로모션을 설정할 수 있죠 이건 이름을 '무료 1개월'이라고 짓고 1개월간 무료로 제공하는 프로모션으로 만들게요
App Store Connect에서처럼 어떤 고객에게 자격이 있는지 선택하고 신규 프로모션을 이 프로모션으로 갱신할 수 있는지 선택합니다 일단 기본 설정으로 놔두죠 코드를 설정했으니 '완료'를 누릅니다 App Store Connect와 동기화한다면 설정한 프로모션이 이 테이블에 자동으로 나타나죠 프로모션을 설정했으니 앱의 스토어 뷰를 탐색합시다 뷰의 아래쪽에 구독 프로모션을 갱신하는 버튼을 추가했죠 스토어 뷰에 적용한 걸 Xcode에서 열면 프로모션 코드를 적용할 때 offerCodeRedemption 제어자를 뷰에 추가하고 버튼을 터치했을 때 isPresented 바인딩을 true로 전환하면 됩니다 어떻게 작동하는지 보죠
버튼을 누르면 앱 위로 갱신 시트가 나타납니다 App Store에서는 App Store Connect에서 생성한 프로모션 코드를 입력할 수 있지만 Xcode에서의 테스트 경험은 훨씬 간소화되어 있죠 설정 파일에 모든 프로모션 코드 목록이 있고 잠금 해제한 구독으로 묶여 있습니다 갱신하려면 방금 생성한 프로모션을 탭 하고 갱신 버튼을 누르십시오 결제 시트가 나타나며 프로모션 코드는 신규 프로모션이 끝난 뒤 시작하는 걸 볼 수 있죠
구독 후에는 확인 화면이 나오며 이제 시트를 닫고 앱의 '소셜 피드 '의 기능에 접근할 수 있는지 봅시다
이 거래에 관한 인스펙터를 살펴보면 현재 신규 프로모션이 적용된 걸 볼 수 있죠 기간만큼 돈을 내는 프로모션이므로 갱신 섹션을 보면 신규 프로모션이 2회 갱신된다고 나옵니다 그 이후에는 방금 입력한 무료 1개월 코드가 적용되죠 이후에는 무기한으로 기본 구독이 자동 갱신됩니다 인스펙터에는 현재 구독 상태에 관해 명확하게 기술돼 있는데 프로모션이 중복되는 경우도 설명이 잘돼 있죠 로컨 StoreKit 설정에서 프로모션 코드 설정법을 살펴보고 iPhone에서 코드 사용을 테스트하는 법도 봤습니다 프로모션 코드는 기존 및 잠재 구독자에게 유연한 프로모션을 제공하는 좋은 방법이며 푸드 트럭의 프로모션 코드를 이용하여 쉽게 시작할 수 있죠 이제 StoreKit를 이용하여 프로모션을 관리하는 법을 볼게요 코드를 사용하면 Transaction.updates와 Status.updates 시퀀스가 새로운 값을 내보내죠 거래 값의 offerType 속성을 확인하여 현재 거래에 프로모션이 적용됐는지 볼 수 있습니다 방금 본 사례에서는 offerType이 신규 프로모션인데 프로모션 코드를 신규 프로모션과 중복해서 사용할 수 있게 설정했기 때문이죠 renewalInfo 값에서 offerType 속성을 확인하여 다음 갱신 때 어떤 프로모션이 적용되는지 볼 수 있습니다 방금 본 사례에서는 초깃값이 신규 프로모션인데 기간만큼 돈을 내는 프로모션을 사용했기 때문이죠 2회의 구독 기간이 지나면 값이 코드로 바뀔 건데 코드를 통한 프로모션이 중첩되어 있기 때문입니다 offerType이 코드면 offerID 속성을 이용하여 프로모션 코드로 적용된 참조 이름을 구할 수 있죠 Xcode에서 프로모션 코드를 테스트하는 방법이었습니다 프로모션 코드 설정은 Xcode 13.3부터 가능하며 iOS 15.4 이후 버전을 사용하는 iPhone과 iPad에서 테스트하세요 푸드 트럭의 프로모션 코드가 작동하는 걸 확인했으니 소셜 피드 의 가격 인상은 어떻게 처리하는지 테스트하죠 Xcode에서 가격 인상을 테스트하는 건 간단합니다 먼저 소셜 피드 구독의 월간 가격을 인상할게요
이 단계는 선택 사항입니다 가격을 그대로 두고도 가격 인상을 테스트할 수 있죠 거래 관리자로 돌아가서 가장 최근의 구독 거래를 선택하고 툴바에서 '가격 인상 요청 동의'를 클릭하세요
거래 관리자에서 보면 현재 거래가 '가격 인상 대기 중' 상태로 바뀌었습니다 그리고 기기를 보면 앱 위에 시트가 나타나는데 가격 인상에 동의하는지 묻는 내용이죠 이 시트는 코드를 추가하지 않아도 알아서 나타나지만 Messages API를 이용하여 동작을 커스텀화했습니다
코드에 Messages API를 어떻게 통합했는지 살펴보죠
for 루프로 메시지 시퀀스를 반복하고 가격 인상과 같은 메시지를 받으면 도넛 에디터와 같은 민감한 뷰 위에 나타나지 않도록 합니다 아니면 DisplayMessageAction을 사용하여 메시지를 보여 주죠 도넛 에디터를 사용 중이라면 메시지 값을 가지고 있다가 도넛 편집이 끝난 후에 나타나게 합니다
이제 테스트로 돌아가죠
App Store에는 기존 구독자가 결정을 내릴 때까지 여러 차례 가격 인상 메시지를 받을 수 있으며 가격 인상에 동의하거나 취소할 수 있습니다 Xcode로 메시지의 노출 시점을 제어할 수 있죠 거래 관리자의 버튼을 누를 때마다 메시지를 받을 수 있는데 이미 가격이 인상된 상태에도 메시지가 나타나죠 이제 유예 로직이 실제로 작동하는지 테스트할게요 도넛 에디터를 열고
메시지를 보내 시트를 열겠습니다
시트가 나타나지 않지만 도넛 에디터에서 나가면 예정대로 시트가 나타나죠 가격 인상을 받아들이거나 구독을 취소할 수 있지만 실제로 사용자는 이메일 같은 외부 소스로 가격 인상에 반응할 수도 있죠 이를 시뮬레이션하기 위해 거래 관리자에 있는 승인 및 거절 버튼을 사용하겠습니다 도넛 편집 경험이 정말 마음에 들어서 거래 관리자에서 승인을 눌러 새로운 가격에 동의했어요 Xcode의 StoreKit를 사용하면 가격 인상과 같은 복잡한 코너 케이스도 쉽게 테스트할 수 있습니다 이제 가격 인상을 시뮬레이션하는 걸 봤으니 StoreKit로 앱에서 가격 인상을 처리하는 법을 살펴보죠
가격 인상 상태를 테스트할 때 상태 업데이트 시퀀스가 상태가 바뀔 때 새 값을 내보내죠 앱에서 이런 업데이트를 감지하려면 RenewalInfo 값의 priceIncreaseStatus를 확인하세요 만약 가격 인상으로 인해 고객이 구독을 취소하면 감지할 수 있는데 expirationReason 속성의 didNotConsentToPriceIncrease를 확인하면 됩니다 가격 인상을 테스트할 때 유닛 테스트를 작성할 수도 있죠 시작할 때 대화 상자를 비활성화하면 앱 위로 가격 인상 UI가 나타나지 않게 테스트할 수 있고 구독을 구입한 후에는 requestPriceIncreaseConsentFor Transaction API를 사용하여 절차를 시작하고 구독을 위한 최신 거래의 ID를 통과시킵니다 가격 인상을 앞둔 거래의 테스트를 인증하기 위해 isPendingPriceIncreaseConsent 속성을 확인하죠 마지막으로 테스트하는 항목에 따라 consentToPriceIncreaseFor Transaction을 호출하거나 declinePriceIncreaseFor Transaction을 호출하여 가격 인상이 완료된 사례에 대한 앱의 반응도 볼 수 있습니다 가격 인상 테스트를 모두 살펴봤네요 모든 플랫폼에서 Xcode 13.3으로 가격 인상을 테스트할 수 있죠 가격 인상 메시지는 iOS 15.4 이후 버전에서 테스트할 수 있습니다 마지막으로 구독 청구 재시도와 유예 기간을 살펴보죠 청구 재시도는 구독 갱신을 시도하다가 만료된 신용 카드처럼 오류가 발생한 상태입니다 App Store에서는 청구 재시도 중에 App Store가 문제를 해결하고 구독을 복원하려고 시도하죠 선택적으로 구독 유예 기간을 활성화하여 청구 재시도 초기의 제한된 기간 중에는 고객의 구독 유지를 허용하는 겁니다 Xcode 테스트에서 시뮬레이션하는 법을 보여드리죠 구독 갱신 중 청구 문제를 재현하려면 테스트 중인 StoreKit 설정의 에디터 메뉴를 열고 '갱신 시 청구 재시도'를 활성화하세요
저는 푸드 트럭 앱이 청구 유예 기간을 지원하도록 이 메뉴의 '청구 기간 유예'도 활성화하겠습니다
구독 진행 속도를 높여 상태가 어떻게 바뀌는지 보죠
먼저 소셜 피드 를 구독하고
구독 갱신 시점까지 기다립니다
거래가 만료되면 청구 유예 기간 상태에 진입하죠 거래 인스펙터를 보면 각 상태의 종료 시점이 나옵니다
청구 유예 기간은 방금 만료됐고 일반적인 청구 재시도 상태가 됐죠 언제든지 '거래 문제 해결' 버튼을 이용하여 오류 해결을 시뮬레이션할 수 있습니다 문제 해결을 테스트하죠
이제 문제가 해결되어 새로운 거래가 이루어집니다
'갱신 시 구독 재시도'가 활성화되어 있어 새로운 거래는 청구 재시도 상태에 진입하며 이런 테스트를 원하는 만큼 반복할 수 있죠 청구 재시도와 유예 기간을 잘 사용하면 비자발적 이탈을 줄여 구독자를 유지할 수 있습니다 Xcode로 이런 상태를 재현하는 게 얼마나 직관적인지 살펴봤으니 StoreKit를 이용하여 어떻게 처리하는지 보죠 청구 재시도와 유예 기간 상태가 변하면서 상태 업데이트 시퀀스가 새 값을 내보냅니다 푸드 트럭에서도 유예 기간을 제공하므로 유예 기간 중에는 구독자들이 소셜 피드 를 이용할 수 있어야 하죠 구독자의 유예 기간을 얼마나 길게 설정할지는 renewalInfo의 속성인 gracePeriod ExpirationDate를 사용하세요 청구 재시도를 확인하려면 isInBillingRetry를 보면 됩니다
상태 속성인 Status를 통해 각 상태를 감지할 수 있죠 고객이 이 상태 중 하나라면 딥 링크로 App Store에 안내하여 문제 해결을 안내할 수 있습니다 현재 currentEntitlements API를 사용하신다면 유예 기간 중에는 만료된 구독에 대한 거래를 수신하게 되죠 유닛 테스트에서 청구 재시도와 유예 기간을 제어하려면 StoreKit 세션에서 billingGracePeriodIsEnabled와 shouldEnterBillingRetry OnRenewal을 설정하세요 구독이 청구 재시도 상태에 진입한 걸 앱이 감지하면 테스트 거래의 hasPurchaseIssue 속성이 true일 겁니다 다양한 상태 업데이트를 기다리고 예정된 앱 업데이트를 진행했다면 resolveIssueForTransaction 메서드를 이용하여 App Store가 구독을 복구하는 걸 재현할 수 있죠 Xcode 13.3 이후 버전에서 청구 재시도, 유예 기간을 모든 플랫폼에서 테스트할 수 있습니다 세션 후반부에서 피터가 iOS와 iPadOS 16의 샌드박스에서 이 상태의 테스트를 자세히 다루죠
지금까지 다룬 상급 테스트 사례로 환불 요청과 청구 재시도, 유예 기간까지 살펴봤습니다 신규 StoreKit API 활용법에 관한 자세한 내용을 통해 이런 사례를 지원하고 싶다면 '앱 내 구입의 새로운 소식'을 시청하십시오
올해 추가된 Xcode StoreKit 테스트에 관해 빠르게 살펴봤지만 모든 걸 다루지 않았습니다 구독 갱신율도 새로 추가됐고 StoreKit 2의 앱 내 구독 시트를 Xcode에서 테스트할 수 있으며 StoreKitTest로 SKAdNetwork 적용에 관한 유닛 테스트도 작성할 수 있죠 'SKAdNetwork 새 소식'에서 더 알아보세요 이제 피터가 올해 추가된 샌드박스 테스트 환경에 관해 소개할 겁니다 고마워요, 그레그 안녕하세요, 저는 피터입니다 App Store 서버 엔지니어죠 Xcode StoreKit Testing의 새 기능으로 복잡한 앱 내 구입 적용을 테스트할 수 있죠 여러분의 피드백을 계속 경청하고 있으며 많은 분이 App Store 샌드박스 환경을 이용하여 앱 내 구입과 서버 적용 사항을 테스트한다는 걸 압니다 앞으로 공유해 드릴 샌드박스 개선 사항을 통해 온라인 테스트 환경에서 앱과 서버를 쉽게 테스트할 수 있죠 샌드박스 Apple ID 생성 개선을 소개하고 App Store Connect API와 청구 실패 시뮬레이션을 진행할 겁니다 샌드박스 환경을 사용하려면 App Store Connect에서 샌드박스 Apple ID를 설정하세요 샌드박스 테스터 목록을 사용자와 접근 페이지의 내비게이션 바로 옮겼죠 여기서 '플러스' 버튼으로 새 테스터를 생성하세요 새로운 테스터 창에서 몇 가지 항목을 제거하여 생성 절차를 간소화했죠 이제 최소한의 정보만 요구하여 불필요한 정보 입력 없이 계정을 생성할 수 있습니다 이메일 주소의 '플러스' 기호를 이용하여 테스터마다 새로운 이메일 주소를 생성하지 않아도 되죠 강력한 비밀번호 생성 절차도 간소화했습니다 안전한 비밀번호 설정에 대한 안내 문구를 추가했죠
Apple ID 생성 양식의 간소화와 강력한 비밀번호를 위한 힌트 제공으로 계정 설정 시간을 줄이고 앱 개발에 투자하시기 바랍니다 App Store Connect는 샌드박스 Apple ID를 생성하고 관리하는 곳으로 앱 콘텐츠와 조직을 관리할 수도 있죠 지난 몇 년간 여러분이 요청한 기능을 샌드박스에 추가했는데 샌드박스 계정 지역 변경과 구매 내역 초기화가 있습니다 이런 기능 대부분은 App Store Connect나 기기 내 샌드박스 구독 관리 페이지에서 접근할 수 있죠 올해 중으로 샌드박스 기능 중 여러 가지를 App Store Connect API에 적용할 건데 샌드박스 Apple ID의 목록을 검색하는 기능과 구입 기록 초기화 중단된 구입 상태 설정 등이 있죠 이를 통해 샌드박스 계정으로 빠르게 테스트하고 일반적인 테스트 도구로 자동 클라이언트를 설정합니다 마지막으로 샌드박스에서 청구 실패 시뮬레이션을 지원하죠 2018년, 자동 갱신 구독의 비자발적 이탈을 막기 위해 청구 재시도와 유예 기간 기능을 도입했습니다 2019년에 기능을 도입한 이래 청구 유예 기간을 통해 300만 일의 고객의 유료 서비스를 복원했죠 여러분의 사업에는 지속적인 수익을 가져다주고 고객의 서비스는 중단되지 않았습니다 많은 분이 제작 단계에서 청구 실패 사례를 처리하지만 샌드박스에서 테스트 시나리오를 더 많이 제공하여 청구 실패를 테스트하고 처리한 뒤 App Store에 앱을 출시할 수 있죠 새로운 샌드박스 계정 설정 페이지를 이용하여 계정의 청구 실패 시뮬레이션을 활성화할 수 있고 앱의 맥락 안에서 포그라운드와 백그라운드의 구독 실패를 테스트하고 verifyReceipt와 App Store 서버 API 샌드박스의 App Store 서버 알림 V2로 구독 상태를 인증할 수 있습니다 청구 재시도와 비자발적 이탈 감소에 관한 정보는 2018 WWDC 세션인 '구독 설계하기'를 참고하십시오 올해는 샌드박스 계정 설정에 새로운 스위치를 도입하여 실패한 앱 내 구입 시도를 시뮬레이션합니다 샌드박스 구독 페이지의 새로운 보금자리죠 청구 실패 시뮬레이션을 활성화하면 포그라운드 앱 내 구입이 실패합니다 고객의 결제 수단이 승인 거부됐을 때와 같은 방식으로 작동하죠 청구 실패 시뮬레이션으로 자동 갱신 구독 상태를 제작 기간 중 청구 실패와 일치시킬 수 있습니다 또한 청구 문제를 겪는 고객에게 발송되는 앱 내 메시지를 테스트할 수도 있죠 이러한 구독 상태는 V2 알림으로 인증한 앱 내 구입 영수증에 반영됩니다 이제 구독 생명 주기를 검토해 보죠 샌드박스에서 자동 갱신 구독을 구입하면 V2 알림을 수신합니다 SUBSCRIBED나 DID_RENEW죠 실패한 앱 내 구입 시도를 테스트할 때 계정에 활성화된 구독이 있다면 다음 갱신 때 청구 재시도 상태가 됩니다 샌드박스에서 청구 재시도 알림을 수신하죠 DID_FAIL_TO_RENEW와 같은 알림입니다 구독 갱신 복구 시도를 중단하기 전에 청구 실패 시뮬레이션을 비활성화하면 다음 재청구 시도는 성공하며 DID_RENEW 알림을 수신하고 하위 유형은 BILLING_RECOVERY죠 만약 재시도 횟수 한도에 도달했고 실패 시뮬레이션이 활성화 중이면 구독이 만료되며 EXPIRED를 수신하고 하위 유형은 BILLING_RETRY입니다 이미 제작에서 유예 기간을 사용 중이고 샌드박스에서 V2 알림을 사용한다면 DID_FAIL_TO_RENEW 알림을 수신할 수 있으며 하위 유형은 GRACE_PERIOD죠 청구 재시도 상태의 구독에서 유예 기간이 있는 예시입니다 이때 DID_FAIL_TO_RENEW 알림을 수신하며 하위 유형은 GRACE_PERIOD죠 GRACE_PERIOD_EXPIRED는 유예 기간 종료 때도 시뮬레이션이 활성화된 경우입니다 App Store 서버 API의 구독 정보를 인증할 때 signedRenewalInfo의 페이로드를 해독하여 구독 상태를 인증할 수 있죠 expirationIntent와 청구 재시도 항목이 기재돼 있는데 청구 재시도 중 구독 영수증을 /verifyReceipt로 호출할 때 is_in_billing_retry_period 플래그가 1로 설정된 걸 볼 수 있습니다 또한, 유예 기간을 사용하면 유예 기간 만료 날짜가 입력되어 있을 겁니다 샌드박스에서 청구 실패 테스트를 완료했으면 샌드박스 계정 설정에서 스위치를 끌 수 있습니다 새로운 테스트를 이용하여 고객에게 최고의 경험을 제공하십시오 오늘은 여러분이 사용할 수 있는 여러 가지 테스트 기능을 통해 앱 내 구입 기능 테스트를 간소화할 수 있음을 알아봤습니다 Xcode로 App Store Connect 설정을 동기화하여 로컬 테스트나 샌드박스 환경과 같은 앱 내 구입 설정을 사용할 수 있죠 Xcode에서 프로모션 코드 및 환불 테스트 같은 새 기능으로 복잡한 StoreKit 적용 사항을 인증할 수 있습니다 구독 관리 테스트는 여러분의 앱을 발전시켜 서비스가 중단됐을 때도 멋진 고객 경험을 선사할 수 있죠 결제 실패가 구독 수입에 주는 영향과 샌드박스의 App Store 서버 알림 V2가 궁금하시면 WWDC 21 세션인 '서버에서 앱 내 구입 관리하기'를 추천합니다 또한, App Store 서버 API와 V2 알림의 새로운 사항이 궁금하시면 '앱 내 구입 신규 사항'을 보세요 새 기능에 관한 여러분의 피드백을 기다리겠습니다 시청해 주셔서 감사합니다
-
-
6:58 - Subscription option view
VStack(alignment: .leading) { Text(subscription.displayName) .font(.headline.weight(.semibold)) Text(subscription.description) }
-
11:18 - Refund view
struct RefundView: View { @State private var selectedTransactionID: UInt64? @State private var refundSheetIsPresented = false @Environment(\.dismiss) private var dismiss var body: some View { Button { refundSheetIsPresented = true } label: { Text("Request a refund") .bold() .padding(.vertical, 5) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .padding([.horizontal, .bottom]) .disabled(selectedTransactionID == nil) .refundRequestSheet( for: selectedTransactionID ?? 0, isPresented: $refundSheetIsPresented ) { result in if case .success(.success) = result { dismiss() } } } }
-
12:33 - Refunds emit an updated value from the transaction updates sequence
for await update in Transaction.updates { let transaction = try update.payloadValue if let revocationDate = transaction.revocationDate, let revocationReason = transaction.revocationReason { print("\(transaction.productID) revoked on \(revocationDate)") switch revocationReason { case .developerIssue: <#Handle developer issue#> case .other: <#Handle other issue#> default: <#Handle unknown reason#> } <#Revoke access to the product#> } <#...#> }
-
14:21 - Offer code view
struct SubscriptionPurchaseView: View { @State private var redeemSheetIsPresented = false var body: some View { Button("Redeem an offer") { redeemSheetIsPresented = true } .buttonStyle(.borderless) .frame(maxWidth: .infinity) .padding(.vertical) .offerCodeRedeemSheet(isPresented: $redeemSheetIsPresented) } }
-
for await verificationResult in Transaction.updates { guard case .verified(let transaction) = verificationResult else { <#Handle failed verification#> } <#Handle updated transaction#> } for await updatedStatus in Product.SubscriptionInfo.Status.updates { guard case .verified(let renewalInfo) = updatedStatus.renewalInfo else { <#Handle failed verification#> } <#Handle updated status#> }
-
16:31 - Check the active offer on the transaction value
for await status in Product.SubscriptionInfo.Status.updates { let transaction = try status.transaction.payloadValue let renewalInfo = try status.renewalInfo.payloadValue if let currentOfferType = transaction.offerType { switch currentType { case .introductory: <#Handle introductory offer#> case .promotional: <#Handle promotional offer#> case .code: <#Handle offer for codes#> default: <#Handle unknown offer type#> } self.hasCurrentOffer = true } <#...#> }
-
16:49 - Check the next pending offer on the renewal info value
for await status in Product.SubscriptionInfo.Status.updates { let transaction = try status.transaction.payloadValue let renewalInfo = try status.renewalInfo.payloadValue <#Check active current offer#> if let nextOfferType = renewalInfo.offerType { switch currentType { case .introductory: <#Handle introductory offer#> case .promotional: <#Handle promotional offer#> case .code: print("Customer has \(renewalInfo.offerID) queued") <#Handle offer for codes#> default: <#Handle unknown offer type#> } self.hasQueuedOffer = true } <#...#> }
-
18:45 - Messages updates loop
private var pendingMessages: [Message] = [] private func updatesLoop() { for await message in Message.messages { if <#Check if sensitive view is presented#>, let display: DisplayMessageAction = <#Get display message action#> { try? display(message) } else { pendingMessages.append(message) } } }
-
20:53 - Price increase changes emit an updated value from the status updates sequence
for await status in Product.SubscriptionInfo.Status.updates { let renewalInfo = try status.renewalInfo.payloadValue if renewalInfo.priceIncreaseStatus == .agreed { print("Customer consented to price increase") <#Handle consented to price increase#> } if renewalInfo.expirationReason == .didNotConsentToPriceIncrease { print("Customer did not consent to price increase") <#Handle expired due to not consenting to price increase#> } <#...#> }
-
21:19 - Unit testing price increases
let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>") session.disableDialogs = true <#Purchase a subscription#> var transaction: SKTestTransaction! = session.allTransactions().first session.requestPriceIncreaseConsentForTransaction(identifier: transaction.identifier) transaction = session.allTransactions().first XCTAssertTrue(transaction.isPendingPriceIncreaseConsent) <#Assert app updates for pending price increase#> // Write a test case for consenting and cancelling due to price increase: session.consentToPriceIncreaseForTransaction(identifier: transaction.identifier) // OR session.declinePriceIncreaseForTransaction(identifier: transaction.identifier) session.expireSubscription(productIdentifier: "<#Product ID#>") <#Assert app updates for finished price increase#>
-
24:57 - Billing retry and grace period status changes emit an updated value from the status updates sequence
for await status in Product.SubscriptionInfo.Status.updates { let renewalInfo = try status.renewalInfo.payloadValue if let gracePeriodExpirationDate = renewalInfo.gracePeriodExpirationDate, gracePeriodExpirationDate < .now { print("In grace period until \(gracePeriodExpirationDate)”) <#Allow access to subscription#> } else if renewalInfo.isInBillingRetry { <#Handle billing retry#> } <#...#> }
-
25:27 - Using the state property of a status value to check for billing retry states
struct SubscriptionStatusView: View { let currentSubscription: Product let status: Product.SubscriptionInfo.Status @Environment(\.openURL) var openURL var body: some View { Section("Your Subscription") { <#...#> if status.state == .inBillingRetryPeriod || status.state == .inGracePeriod { VStack { Text(""" There was a problem renewing your subscription. Open the App Store to update your payment information. """) Button("Open the App Store") { openURL(URL(string: "https://apps.apple.com/account/billing")!) } } } } } }
-
25:41 - Current entitlement APIs will account for grace period
for await entitlement in Transaction.currentEntitlements { <#Grant access to product#> }
-
25:50 - Unit testing billing retry and grace period
let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>") session.billingGracePeriodIsEnabled = true session.shouldEnterBillingRetryOnRenewal = true <#Purchase a subscription#> wait(for: [<#XCTExpectation#>], timeout: 60) let transaction: SKTestTransaction! = session.allTransactions().first XCTAssertTrue(transaction.hasPurchaseIssue) <#Assert app still allows access to subscription due to grace period#> wait(for: [<#XCTExpectation#>], timeout: 60) <#Assert app detects billing retry and no longer allows access to subscription#> session.resolveIssueForTransaction(identifier: transaction.identifier) <#Assert app allows access to subscription#>
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.