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시간

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이랑 비교해봤자 의미가 없다고 하거나 못 한다고 하기 때문에.. -_- 하지만 위와 같은 원리를 활용하면 비교가 가능합니다.

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

iOS 앱 배포 시 Fetching App Store configuration 문제

사내 프로젝트든, 개인 프로젝트든 iOS 앱 프로젝트를 진행하면 애플과의 앱 리뷰 대전쟁 이전에 더 짜증나는 과정이 기다리고 있다.

대체 이 과정에서는 뭘 하길래… 1시간 2시간이 걸릴 때도 있고 심하면 그러다가 맥이 다운되면서 재부팅이 되더라… 하는 것이다.
보통 이때 쯤 멘탈이 터지는데 필자는 여러번 터져봐서 이제는 면역이 되었다 (..)
구글링해서 사례를 찾아도 15분 기다리는건 보통이고 몇 시간 걸리는 사람이 종종 나타나는데 1페타바이트 파일을 받는거도 아니고 대체 뭘 하는 걸까. (아니면 내가 꼬린 사양의 맥만 사용해서 그런 걸 수도..)

여하튼, 저 과정이 1시간 2시간이 지나도 진행이 안되고 저 화면에서 멈출 수 있는데, 그럴 땐 아래 방법을 사용 해 보자. 본인도 딱 한번 해봤는데, 1시간 지나도 안 되던게 10분만에 업로드 체크 화면이 뜨더라.

0. 혹시 저 화면에서 멈춰있는 상태이고 더 기다리기 힘들다면 Cancel를 누르자. 그런데 아마 안 눌릴거다. Option + Xcode 우클릭으로 강제 종료를 시켜주자
1. 이후 터미널을 열고 아래 명령을 입력 해 보자

2. iTMSTransporter가 알아서 파일 업데이트를 진행하는데, 다 되면 명령줄 도움말이 나타날 것이다. 무시하고, Xcode에서 다시 배포를 시도 해 보자

이게 본질적인 해결 방법은 아닌 것 같지만 그렇다고 해결이 가능한 방법이 딱히 존재하지는 않는 것 같다. 추측이라면 할 수 있는데, 외장 SSD를 부팅 디스크로 사용해서 그런가 macOS Mojave로 업데이트 한 뒤부터 부팅 시간도 15초 내외이던게 5분에서 10분이 걸리고 정신이 없다..
( 사내 개발환경이 내장 디스크가 하드 디스크인 꼬린 아이맥이여서 어쩔 수 없이 외장에 SSD를 달고 산다 ㅠㅠ )

앱 배포 시 다른 작업을 할 수 없는 점은 아직도 익숙해지지 못하고 있다. 이거도 개발환경 맥이 구려서 그런건진 모르겠다만, 저 화면에서 빙빙 돌아갈 때 아이맥이 쓸 수 없을정도로 느려지기 때문에 어쩔 수 없이 폰으로 유튜브 등을 보면서 시간을 때우는데 그게 한시간 두시간 걸리니까 답답해 미칠 지경..

여하튼 도움이 되었으면 좋겠다.

Xcode 9, iOS 11에서 Navigation bar의 이미지 버튼 크기 고정하기

Xcode 9에서 빌드하고 iOS 11로 실행하면서 기존에 만들었던 화면이 원하는 대로 나오지 않는 경우가 있는데, 이런 경우도 있다.

딱 보면 알겠지만, 원하던 그림이 아니다. -_-;;
이게 iOS 10까지는 정상 동작했다는 것에서 많이 해멨는데, Xcode 9 및 iOS 11 이상부터는 widthAnchorheightAnchorconstant를 추가해야 한 다고 한단다.

소스 코드의 값 45는 설정하고 싶은 너비와 높이로 만들면 된다.
navigationButtonItem는 UIBarButtonItem 클래스 객체이므로 왼쪽 버튼이든 오른쪽 버튼이든 이제부터는 무조건 들어가야 하나 보다.
widthAnchorheightAnchor는 iOS 9부터 사용할 수 있으므로 iOS 8 혹은 이전까지의 환성을 고려하고 제작하고 있다면 아래와 같이 if문 등을 통해 버전을 꼭 체크 해 주자.

위와 같이 적용했다면, 이제 제대로 나오는 것을 볼 수 있다.
왼쪽 버튼과 오른쪽 버튼의 여백이 iOS 10 이전보다 넓어진 것 같은 느낌은 들지만 기분탓이려나..