iOS 인 앱 구매 (In App Purchase) 구현 가이드라인

이전에 사내 공유 목적으로 작성한 In App Purchase 가이드라인을 정리하여 보았습니다.

공식 가이드라인(Apple): https://developer.apple.com/kr/app-store/review/guidelines/#business

요약

  1. 인앱결제 수수료(Fee) 30%, 구독상품의 경우 1년이상 구독시 해당유저 15%로 인하
  2. 모든 상품에 현지통화 가격 표시해야 함
  3. 인앱결제 이외의 결제수단은 허용되지 않음. (단, 가상의 서비스가 아닌 실물 상품 등을 제공하는 경우에는 가능), 가상 서비스 재화 등의 경우 쿠폰 코드 입력 화면도 사용할 수 없음.
  4. 인앱 구독상품이 타사 서비스에 영향을 주면 안됨
  5. TestFlight에서의 인앱 구매는 실제 결제가 되지 않기 때문에, 서버에서 별도 처리가 필요

용어 설명

  소모성 제품 (Consumable) 비소모성 제품 (Non-Consumable) 자동 갱신 구독 (Auto-renewable Subscription) 비갱신형 구독 (Non-renewable Subscription)
구매 가능 여부 여러 번 한 번만 여러 번 여러 번
영수증 표시 여부 한 번만 항상 항상 항상
기기간 동기화 여부 X 시스템에 따라 다름 시스템에 따라 다름 앱에 따라 다름
구매 복원 X 시스템에 따라 다름 시스템에 따라 다름 앱에 따라 다름

소모성 제품 (Consumable)

여러번 구입 가능한 제품. 앱 내 결제 복원 시에는 복원되지 않는 항목이며 아이템은 앱이나 서버에서 관리해야 합니다. (Apple에서 관리하지 않습니다)

구현 예시: 앱 내 유료 재화 (예: 보석) 등

앱 내에서만 데이터가 관리되는 경우, 앱 삭제 시 구매한 재화를 돌릴 수 있는 방법이 없으므로 외부 서버 등에서 관리되어야 하며 단순한 기능 잠금 해제 등의 경우 비소모성 제품 (Non-Consumable)을 이용해 구현해야 합니다. 비소모성 제품을 이용해 구현하면 구매 여부를 Apple에서 관리하므로, 별도의 서버 구현이 필요하지 않습니다.

비소모성 제품 (Non-Consumable)

Apple 계정당 한 번만 구입 가능한 제품. 앱 내 결제 복원 시에는 추가 구매없이 (재화 소모나 결제 없이) 복원되어야 하며 구매 항목은 Apple에서 관리합니다.

구현 예시: 광고 제거, 유료 게임 스테이지 잠금 해제 등

인 앱 상품을 구현할 때 구매 항목을 복원할 수 있는 버튼이나 메뉴가 앱에 존재해야 하며, 이 때 어떠한 추가 구매 절차 등이 들어가서는 안 됩니다. 또한, 구매 복원 시 구매한 항목은 바로 복원되어야 합니다.

자동 갱신 구독 (Auto-renewable Subscription)

특정 주기별로 자동 결제되는 제품. 앱 내 결제 복원 시에는 추가 결제 절차없이 복원되어야 하며 구매한 항목은 Apple에서 관리합니다. 사용자가 언제든지 앱을 통하지 않고서도 구독을  해지할 수 있습니다.  (Apple 계정 -> 구독 관리에서 해지 가능)

구현 예시: 일반판에서 전문가판으로 업그레이드 (매 주기마다 결제)

특정 주기별로 여러 개의 구독 상품을 만들 수 있으며 무료 체험 기간 또한 지정할 수 있습니다. 구독 상품을 구매했을 때의 혜택으로 앱의 유료 재화를 구독 기간동안 추가 제공하는 등의 상품도 만들 수 있습니다. 단, 구독 시 이용할 수 있는 컨텐츠의 범위가 확실하게 늘어나야 하며 구독 후 이용할 수 없는 상황이 와서는 안 됩니다.

구독 수수료는 기본적으로 타 항목과 동일한 30%이지만, 유저가 1년 이상 구독 한 경우 수수료는 해당 건에 한해 15%로 인하됩니다.

구독을 통해 제공하는 항목의 가격이 비정상적으로 비쌀 경우 Apple App Review 단계에서 Reject (거절) 될 수 있습니다.

비갱신형 구독 (Non-renewable Subscription)

한정 기간동안만 상품을 이용할 수 있도록 한 번만 결제하는 제품이지만 만료되면 다시 결제해야하는 제품. 결제 복원 여부는 앱이 결정하며, 구매 항목은 Apple에서 관리합니다. 다시 구독하려면 사용자가 다시 수동으로 결제해야 합니다. (자동으로 갱신되지 않음)

구현 예시: 프리미엄 스트리밍 컨텐츠 구독권

비갱신형 구독 상품의 경우, 앱 로그인을 필수로 요하는 경우 App Reject (거절) 사례가 종종 있다고 합니다.

참조 URL: https://stackoverflow.com/questions/17609347/restore-transactions-for-non-renewing-subscriptions-without-registration

구독 (Subscription)에 대하여

공식 소개 웹사이트: https://developer.apple.com/kr/app-store/subscriptions/

구독이란 금액을 지불하여 일정 기간동안 컨텐츠를 이용할 수 있도록 하는 일종의 서비스 제공 형태입니다. 각 플랫폼 (App Store 혹은 Google Play)에서 제공하는 구독 시스템을 사용하여 구현할 경우 각 플랫폼에서 구독에 대한 관리, 자동 결제 등을 도맡아 해줍니다.

수수료 (Fee)에 대하여

2020년 기준 기본적인 Apple의 인 앱 결제 수수료는 30%

  • 특정 유저가 자동 갱신 구독 (Auto-Renewable Subscription) 상품을 1년 이상 지속적으로 결제하는 경우, 해당 구독 건에 한해 그 이후의 결제분부터 수수료가 15%로 인하됨(단, 중간에 해당 유저가 정기 구독 결제를 취소한 후 60일이 지나면 결제 기간 누적이 취소됨)
  • 실제로 개발자가 받는 (추정) 수익금은 매출 – 수수료 – (세금)
  • 고객의 DCC (이중 환전 수수료)는 개발자가 지불하는 수수료와 아무 연관이 없음

가이드라인 공통사항

아래 내용은 인 앱 결제 구현 시 반드시 지켜야 하며 위반할 경우 앱이 거절 (Reject) 되거나 App Store에서 삭제될 수 있습니다.

  • 앱 내 구입 항목은 각 소속 앱 스토어 국가에 따른 화폐 단위 및 가격구매 버튼 혹은 아이템 설명에 표시되어야 합니다.

화폐 단위와 가격 표시는 하드 코드할 필요가 없으며, StoreKit API에 각 사용자별 App Store 지역에 따라 화폐 단위와 가격을 표시할 수 있도록 하는 기능이 내장되어 있습니다.

  • 앱에서 쿠폰 코드, 외부 결제 시스템 사용 등을 이용한 아이템 잠금을 구현해서는 안 됩니다.
    • 가이드라인에서는 In App purchase를 사용하여 잠금 해제하도록 강제하고 있습니다.
    • 다른 방법으로 결제하는 것을 유도해서도 안 됩니다. (계좌번호, 연락처 등을 표시하거나 다른 방법을 안내하는 문구 등)
    • 일부 게임, 앱 등에서는 쿠폰 코드 입력 화면을 고객센터 안쪽 링크로 감추거나, 심사 시에만 메뉴를 감추는 방법으로 우회 구현하고 있습니다. 
    • 이 사항으로 앱이 거절될 경우, 앱 심사 담당자에 따라 고객센터 전화번호나 메일 등이 들어가도 거절이 될 수 있습니다. (Apple 앱 심사는 일반적으로 앱 거절 시엔 담당자가 변경되지 않습니다.)
    • 쿠폰 코드를 사용하고 싶은 경우, App Store에서 제공하는 프로모션 코드를 통해 쿠폰 코드의 대체제로 사용할 수 있습니다. (https://help.apple.com/app-store-connect/?lang=ko#/dev50869de4a)

일부 게임에서는 소비 재화를 iOS 플랫폼에서만 수수료를 포함한 30% 인상된 가격에 판매하고 있습니다. (다른 결제 방법을 사용할 수 없기 때문에)

  • 상품이 앱 외부에서 사용하는 상품인 경우에는 앱 내 구입 방법 이외의 방법을 사용해야 합니다. 예를 들면:
    • Apple Pay
    • 신용 카드, 앱 결제
    • 기타 앱 내 구입 이외의 결제 방법

예: G마켓, 옥션, 쿠팡 등의 앱이 상품(물건) 구매를 위해 인 앱 결제를 사용하지 않음

  • 소비자가 구매한 항목은 연락처 업로드, 게시글 업로드, 특정 횟수만큼 앱 로그인 등의 어떠한 조건 없이 바로 사용이 가능해야 합니다.

예: 구매 후 미션을 클리어해야 구매한 항목을 사용할 수 있게 되는 등의 제약

소모성, 비소모성 제품 (Consumable, Non-Consumable)

  • 앱 내 크레딧(소모성 재화) 등은 사용 만료 기한이 없어야 하며 소모성 아이템 이외의 결제 항목모두 결제 복원이 가능해야 합니다. (구매 항목 복원 버튼의 구현 필요)

구매 항목 복원은 StoreKit API를 통해 구현할 수 있습니다. 

  •  인앱 결제 아이템은 다른 유저에게 선물로 보내는 목적으로도 개발할 수 있습니다.
  • 가챠(랜덤성 뽑기 / 랜덤박스) 아이템은 각 보상의 확률을 무조건 공개해야 합니다.
  • 비소모품 제품의 경우 ~일 체험 아이템을 만들 수 있으며 잠금 해제 후의 기능 명시가 확실하게 되어 있어야 하고, 체험 기간이 끝나면 얼마를 지불해야 하는지 명확히 표시해야 합니다.

구독형 제품 (Subscription)

  • 최소 구독 기간은 7일이여야 합니다.
  • 구독한 아이템의 내용이 타 서비스의 이용권/구독권으로써 이용될 수 없습니다.
  • 구독을 통해 소모성 크레딧 등을 제공할 수 있으며 소모성 크레딧 구매 등의 할인권으로써도 판매가 가능합니다.
  • 자동 갱신 구독은 무료 체험 기간을 제공할 수 있습니다.

가격 책정 (Pricing)

모든 상품의 가격은 정해진 등급과 대체 등급의 가격 중에서 선택해야 합니다. (약 200개 정도 있습니다.)

참고 문서: https://help.apple.com/app-store-connect/?lang=ko#/dev2acb77fcc

Apple에서 In App Purchase 상품을 심사할 때, 상품이 적절한 가치를 가지고 있는지도 평가합니다. 만약 가격이 너무 비싸다고 판단할 경우 App Reject이 될 수 있습니다.

영수증 검증 (Validate Receipt)

탈옥 (Jailbreak), 중간자 공격(man-in-the-middle attack) 등의 이유로 실제로 구매하지 않았으나 구매 처리가 되는 경우를 방지하기 위해 구입한 항목에 대해서 영수증 검증 처리가 있어야 합니다.

관련 문서:

API 문서: https://developer.apple.com/documentation/appstorereceipts/verifyreceipt

다계정 처리 (Multiple Apple Accounts)

한 사용자는 다수의 Apple 계정을 가질 수 있습니다. 영수증 데이터에 포함되지 않는 소모성 제품을 제외한 나머지 제품은 다수 계정에 대해 주기적으로 영수증 확인 및 결제 상태에 대한 상태 업데이트를 해 주어야 하며 이는 Receipt 데이터를 주기적으로 서버에 보내어 검증하는 것으로 해결할 수 있습니다.

관련 문서:

환불 처리 (Refund)

이전까지는 결제 항목에 대한 환불 여부를 알 수 없었고, 구독 취소에 대해서만 확인할 수 있었지만 2020년 WWDC부터 모든 유형의 결제 항목에서 환불 알림을 받을 수 있게 되었습니다.

관련 문서: https://developer.apple.com/documentation/storekit/in-app_purchase/handling_refund_notifications

결제 테스트 (Testing In-App Purchase)

결제 테스트는 App Store Connect에서 세금 및 지불 정보 관리가 모두 되어 있어야 결제 테스트가 가능하며 되어있지 않다면 테스트가 불가능 합니다. (결제시 오류 발생)

샌드박스 유저 및 TestFlight에서의 인앱 구매

샌드박스 유저로 인 앱 구매를 시도하거나, TestFlight를 통해 받은 앱에서는 인앱 구매 시 어떠한 대금도 청구되지 않습니다. 영수증 검증 시 TestFlight의 영수증을 서버에 첨부하면 테스트 구매라는 오류 코드가 발생하고, 테스트 서버에 첨부하면 정상 처리가 됩니다. 구매한 상품을 서버에서 관리하기 때문에 테스트 구매 또한 서버에서 별도 처리가 진행되어야 합니다.

구독 테스트 기간 (Sandbox subscription duration)

샌드박스 계정으로 결제하거나, TestFlight를 통한 테스트 결제 시 적용됩니다.

실제 구독 기간 테스트 시 기간
1주 3분
1개월 5분
2개월 10분
3개월 15분
6개월 30분
1년 1시간

GPD WIN MAX 펀딩 시작

GPD WIN, GPD Pocket를 개발한 GPD사에서 GPD WIN MAX의 펀딩을 공식적으로 시작했습니다.

펀딩 바로가기 (Indiegogo)

한화 약 960,000원 정도 (16GB RAM + 512GB SSD)이며 수량 한정이 있는 듯 합니다만 추가 물량이 풀리면 배송이 늦어질 듯 합니다. 이번 분 배송은 펀딩 종료 후 시작된다고 합니다.

기체의 스펙은 기존과 알려진 대로 동일합니다.

GPD WIN MAX 스펙 및 가격에 대해

GPD WIN MAX (GPD WIN 2의 직계 후속작은 아니며 번외편 같은 느낌)의스펙 및 대략적인 가격 정보가 속속 등장하고 있습니다. 스펙은 예전부터 조금씩 공개된 느낌이고 가격은 며칠 전에 중국 현지 가격이나 크라우드 펀딩 시 가격이 비공식인지 공식인지 알 수 없는 상황에서 나왔는데 신빙성이 없지는 않아 보입니다. 따라서 이 포스팅의 스펙과 가격이 출시 시와 완벽히 일치하지 않을 수 있습니다.

Name GPD WIN MAX
Display 8 inch, 10 points multitouch
CPU Intel Core(TM) i5-1035G7 (1.2GHz ~ 3.7 GHz)
GPU Intel Iris Plus Graphics 940
RAM 16GB LPDDR4X 3733
Storage 512GB M.2 SSD
TDP 15W, 20W, 25W (Adjustable)
WLAN / BT Wi-Fi 6 / Bluetooth 5.0
Ports 1x Thunderbolt 3, 1x USB-C 3.1 Gen 2, 2x USB-A 3.1 Gen 1. 1x microSDXC (A2), 1x HDMI 2.0b, 1x RJ45
Battery 57Wh, 11.4V == 5000mAh x3
Size / Weight 207x145x26mm / 790g
Price

779$ (indiegogo, from May 18 (excepted)) / 4,999 CNY ~ 5,799 CNY

Thunderbolt 3을 지원하기 때문에 eGPU등의 사용이 가능하며 아래는 GPD 공식 GPD WIN MAX 게임 데모 비디오입니다.

아래 링크는 GPD WIN MAX의 언박싱 및 핸즈온 영상입니다.

https://www.ixigua.com/i6820684546783052295/

인디고고(indiegogo) 펀딩 페이지가 아직 나오지는 않았지만 타 매체에서는 5월 18일에 시작할 것으로 예상하고 있습니다. 개인적인 의견으로는 무게가 무겁고 사이즈가 크기 때문에 GPD WIN 3을 기다리는게 더 좋을 듯 하네요. 대신 키보드 사이즈가 크기 때문에 키보드를 많이 사용해야 하는 게임을 즐겨 하시는 분이면 이번 제품이 메리트가 있을 수 있다 생각합니다.

Non-Optional Any가 nil인지 체크하기

swift에는 nil (NULL)이 될 수 있는 Optional 형태, 될 수 없는 Non-Optional 형태로 데이터형을 지정할 수 있습니다. 간단하게 데이터형 뒤에 물음표만 붙이면 되는데, 신기하게도 nil이 될 수 없다고 생각하지만 사실 nil이 될 수 있는(??) 형태가 있습니다.

Any

일반적으로 코드를 작성할 때 Any 타입을 사용하는 경우는 많지 않습니다만, 절대적으로 필요할 때가 있습니다. 제가 제일 많이 사용하는 경우는 서버에 API Request 시 보내는 Parameter를 [String:Any] 형태로 받고 있습니다. 어쩄든 일반적으로 생각한다면, Any라고 명시하면 nil이 될 수 없는 형태 (Non-Optional)가 되겠죠?

실제로 위와 같은 형태로 작성하면 오류가 나며, ?를 붙이라고 조언까지 합니다.

하지만 아래같은 경우는 Any 타입임에도 불구하고 nil이 들어갈 수 있습니다. 이러한 상황 뿐만 아닌 다양한 상황에서도 들어갈 수 있죠.

왜 이런 상황이 벌어질 수 있냐에 대한 답은 의외로 간단합니다. 왜냐면 Any는 모든 값이 될 수 있기 때문에(…?) Optional 값도 들어갈 수 있는 것입니다. 

사실 데이터형 뒤에 물음표(?)를 붙이면 Optional<형태> 와 같은 열거형의 형태가 되고 실제로는 이런 모습처럼 됩니다.

Any 형태 역시 nil이 될 수 있는 Any? 와 같이 명시할 수 있지만 Any 형태로도 nil가 될 수 있는 이유는 모든 형태가 될 수 있기 때문이기 때문입니다. 실제로 위의 코드 예제는 아래와 완벽히 같다고도 할 수 있습니다.

본론으로 넘어가서, 그럼 단순하게 Any가 nil인지 아닌지 체크하려면 그냥 이렇게 하면 되지 않을까? 라고 생각할 수 있습니다.

하지만 이는 무조건 실패합니다. Non-Optional 변수를 nil이랑 비교해봤자 의미가 없다고 하거나 못 한다고 하기 때문에.. -_- 하지만 위와 같은 원리를 활용하면 비교가 가능합니다.

생각해보면 참 복잡한 형태가 되었는데.. 사실 이런 경우가 많지는 않다고 생각합니다. 같은 상황이 되었다면 저처럼 헤매지 마시고 부디 이 글을 참고하시면 좋을 듯 합니다.