Tag Archives: Objective C

iOS In-App Purchase 최적화 하기

이 글은 WWDC 2014에서 발표된 Optimizing In-App Purchases의 내용을 단순 정리한 내용입니다. iOS에서는 StoreKit 을 사용하여 이 앱내 구매 기능을 구현할 수 있으며 다음과 같은 기능을 구현할 수 있습니다.

  • In-App Purchase : 앱내 구매
    • Consumable 및 Non-Consumable 상품 : 소진 가능한 상품 및 소진 불가능한 상품
    • Subscriptions : 구독
  • Store Product Sheet : 당신의 앱 내부에서 또다른 앱이나 컨텐츠를 아이튠즈 또는 앱스토어를 통해 판매할 수 있게 해주는 기능
  • Receipt Renewal : 기존의 구매 내역을 복구하는데 사용, 비지니스 모델&불법 복제 방지등을 적용할 수 있는 강력한 방법

이 글에서는 우선 앱내 구매(In-App Purchase)에 대해서만 정리해 보았습니다.

앱내 구매 API에 새롭게 추가된 부분

앱내 구매에 대해 이야기 하기에 앞서 다음과 같은 기능들이 2014년에 새롭게 추가되었습니다.

  • StoreKit Product Sheet는 제휴 프로그램을 지원하며, 기존에는 단순 다운로드 기능만을 제공하였지만 이제는 다운로드에 대한 커미션을 받을 수 있습니다.
  • 새로운 구매 상태 Deffered가 추가되었습니다. 18살 미만의 사용자가 구매시에 가족의 동의를 받아야 하며 부모의 동의 이전에 받게 되는 상태입니다.

SKPaymentTransactionStateDeferred 상태는 아직 구매가 성공한 상태도, 실패한 상태도 아닙니다. 부모가 원격으로 구매에 대한 수락/거부의 결과에 따라 성공 또는 실패를 담은 최종 상태가 미래에 다시 도착하게 됩니다. 즉, 이 상태는 구매를 시작하고 구매가 완료되기까지의 중간 상태를 의미합니다. 여기서 중요한 부분은 이 상태를 수신한 사용자가 계속해서 앱을 이용할 수 있게끔 구현을 해주어야 한다는 점입니다. 또한 사용자가 이 아이템을 다시 구매하는것도 가능해야 합니다.

optimizing_in_app_purchase_01위의 그림은 18세 미만의 사용자가 앱내 구매를 사용할 경우 부모에게 구매 요청의사를 묻는 과정을 표시한 그림입니다. 구매 API를 호출하면 우선 SKPaymentTransactionStateDeferred가 떨어지게 됩니다. 이후에 부모가 수락할 경우 SKPaymentTransactionStatePurchased, 거부할 경우 SKPaymentTransactionStateFailed로 상태가 업데이트 됩니다.

앱내 결제 최적화 방안

optimizing_in_app_purchase_03

앱내 구매를 진행하기 위해서는 위와 같은 플로우를 따릅니다. (왜인지 WWDC의 멋진 자료를 똑같이 그렸음에도 엄청 없어보이는군요.) 최초에 판매하는 인앱 상품들의 식별자를 로드하고 로컬라이징된 상품 정보를 가져옵니다. 이 정보를 가지고 결제 UI를 보여주고 결제를 호출하고 결제 과정이 진행된 후 상품을 지급하고 트랜젝션을 종료하는 과정을 거치게 됩니다.

위의 그림에서 빨간색으로 칠해진 영역이 있는데 이부분이 앱내 구매중에 문제가 발생하게 되는 부분입니다. 이 글에서는 이 부분에서 발생할 수 있는 문제를 해결하는 방법에 대해 알아볼 것입니다.

인앱 식별자 로드

상품 구매를 위해 결제 UI를 보여주어야 하는데 이때에 보여줄 정보를 가져오기 위해 먼저 식별자 세트를 관리하는 방법에 대해 고민해보아야 합니다. 판매하려는 앱내 상품의 앱의 라이프 사이클과 동일하게 변경될 여지가 없는 경우 앱내에 정적으로 보관되는것이 가장 간단한 방법입니다.

하지만 상황에 따라 상품의 변경이 있을 경우 서버로 부터 이 식별자 세트를 받아오는 방법을 취할 수 있습니다. 앱내 구매의 경우 개발자에게는 수익이 창출되는 순간이지만 사용자에게는 한순간의 즐거움(?)을 추구하기 위한 덧없는 순간일 수 있습니다. 이 순간에 딜레이가 발생하거나 오류가 발생하는것은 좋지 않은 결과를 낳게 될 것입니다. 그렇기 때문에 상품의 식별자 정보를 반환하는 서버는 신뢰성을 갖추고 있어야 하며 빠른 결과를 위해 적절한 캐시를 사용하는것이 좋습니다.

optimizing_in_app_purchase_04

또한 아직 구매 가능한 상품 목록이 보여지기 이전에 위와 같은 스피너에 사용자들이 갇히지 않도록 해야 합니다. 위와 같은 화면은 사용자들로 하여금 답답함을 유발 할 수 있습니다. 결론적으로 이 과정은 최대한 문제가 발생하지 않게 빠르게 지나가도록 구현하는것이 중요합니다.

상품정보 가져오기

SKProductsRequest* request = [[SKProductsRequest alloc] 
                               initWithProductIdentifiers:identifierSet];

이제 획득한 상품의 식별자 세트를 이용하여 위와 같은 방법으로 상품 정보가 담긴 SKProduct 객체들을 획득합니다. 이때에는 개발자가 아이튠즈에 미리 등록한 로컬라이징된 상품 이름 및 설명과 가격 정보를 받을 수 있습니다. 또한 컨텐츠 호스트 기능을 사용할 경우 컨텐츠의 사이즈와 버전정보를 받을 수 있습니다. 이 요청은 어쩔 수 없이 딜레이가 발생합니다. 이점을 이해하고 미리 데이터를 예상하여 로드하거나 가능한한 빠르게 결제 화면을 보여줄 필요가 있습니다.

이 세상에는 한가지 상품에 매겨진 다양한 화폐 단위가 존재합니다. 가령 다음과 같은 화폐 단위가 존재할 수 있을것입니다.

1.234,56 €
£1,234.56
€1,234.56
1.234,56 kn
R$ 1.234,56
฿1,234.56
$1,234.56

위의 예시 역시 세상에 존재하는 다양한 화폐 단위중에 매우 작은 예시에 불과합니다. 당신은 사용자들에게 가장 익숙한 화폐단위, 즉 사용자의 국가에 맞는 화폐 단위로 가격을 보여줄 필요가 있습니다. 하지만 일일이 국가별 화폐 단위로 보여주기 위해 신경을 쓰는것은 어려운 일입니다. NSNumberFormatter를 이용하여 SKProduct에 담겨있는 가격정보를 변환하여 보여줄 수 있습니다.

NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:product.priceLocale];
NSString *formattedString = [numberFormatter stringFromNumber:product.price];

위의 코드는 product라는 변수명의 SKProduct객체를 이용하여 화폐단위에 알맞는 가격 표기를 만들어내는 과정을 보여주는 코드입니다. SKProduct객체에는 사용자가 어떤 나라, 어떤 지역에 살고 있는지 알 필요 없이 앱스토어가 적절한 데이터를 내려주게 되어있습니다. 절대로 환율을 직접 계산하지 마십시오.

(주의) 많은 개발자들이 에러를 처리하는 부분에서 잘못하는것을 종종 발견하고 하는데 가장 중요한 핵심은 모든 에러가 동일하지 않다는 것입니다. In-App Purchase 프로그래밍 가이드를 살펴보면 처리 가능한 오류 코드들을 확인할 수 있습니다. 이러한 코드들에 적절한 처리를 분리하여 하시기 바랍니다.

결제 진행중에 발생하는 대부분의 오류 메시지들은 사용자들에게 보여주기 위한 메시지를 담고 있는것이 아니라 구현중인 개발자들에게 보여지기 위한 메시지들입니다. 그러므로 에러메시지를 얼럿창으로 띄워 유저에게 보여주는것은 좋지 않은 방법입니다. 많은 앱들이 “결제요청-터치아이디등을 이용하여 아이디 인증-정말아이템을 구매하시겠습니까? 팝업창-취소” 과정을 거칠 때 수많은 팝업들을 거쳤음에도 불구하고 “취소하셨습니다”라는 팝업을 또 띄우는것을 볼 수 있는데요 이것은 나중에 또다시 구매를 시도할 수 있는 사용자들에게 좀 더 불쾌한 경험이 될 수 있습니다. 사용자가 의도적으로 취소하였기 때문에 “취소하였습니다” 팝업을 또 띄우는것은 무의미합니다.

결제 생성

“결제 UI 보여주기” 단계에서 정말 멋진 결제 화면을 사용자에게 제공하였고 오류도 발생하지 않았습니다. 사용자는 구매하길 원하는 상품을 탭하여 구매를 진행하게 되었습니다. 이후의 과정을 설명해 보겠습니다.

결제의 구현은 실제로 페이먼트큐에 의해 중앙 집중형으로 관리가 됩니다. 페이먼트큐가 스스로 관리를 알아서 해주기 때문에 개발자는 결제 과정의 복잡한 상태를 일일이 체크하거나 캐싱 처리를 할 필요가 없습니다. 스스로 상태를 관리하려 하지 말고 단지 큐를 믿고 사용하시면 됩니다. 큐는 결제 트랜젝션의 진행 상황을 관리해주고 상태가 변할때마다 알려줍니다. 또는 컨텐츠 호스트를 이용할 경우 다운로드 상태를 제공해줍니다. 큐의 모든 트랜젝션들은 유효하고 실존합니다.

여기서 인증되지 않은 사용자의 무단 또는 사기성 구매가 있을 수 있다고 의심될 수 있지만 그것은 다른 문제입니다. 이런 경우는 결제가 완료된 후 영수증을 검증하는 방식으로 해결할 수 있습니다. 어찌되었던 큐를 믿어주시기 바랍니다.

(주의) 가능하면 앱이 구동되는 시점에 SKPaymentQueue의 addTransactionObserver를 실행해 주시기 바랍니다. 여기에 파라미터로 SKPaymentQueue 옵저버 프로토콜에 대응하는 앱내 구매 구현의 핵심이 되는 객체를 사용하게 됩니다.

[[SKPaymentQueue defaultQueue] addTransactionObserver:yourObserver];

이 프로토콜 객체들은 큐 안의 결제들이 어떻게 진행되고 있는지 상태를 전달 받을 수 있으며 에러나 UI 업데이트를 어떻게 할지를 결정할 수 있습니다.

트랜젝션 진행

결제가 진행됨에 따라 트랜젝션의 상태 변경을 델리게이트를 통해 이벤트를 전달 받을 수 있습니다. 결제 성공을 처리 하기 위해서는 다음과 같은 구현이 필요할 것입니다.

- (void)paymentQueue:(SKPaymentQueue *)queue
  updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction* transaction in transactions)
    {
      switch(transaction.transactionState) {
        case SKPaymentTransactionStatePurchasing:
          // 결제가 시작될때 필요한 처리, 결제 버튼이 비활성화 된다던가
          // 따로 상태를 관리하며 UI를 변경하지 말고 여기서 처리하시기 바랍니다.
          ...
          break;
        case SKPaymentTransactionStatePurchased:
          NSURL* receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
          NSData* receipt = [NSData dataWithContentsOfURL:receiptURL];
          // 트랜젝션 처리
          ...
          break;
        case SKPaymentTransactionStateFailed:
          // 결제 실패
          ...
          break;
        case SKPaymentTransactionStateDeferred:
          // 부모의 수락/거부에 따른 다음 트랜젝션 업데이트가 일어날때까지
          // 사용자가 계속해서 앱을 사용할 수 있도록 처리합니다.
          // "구매중..." 같은 팝업창으로 이용을 블로킹 하지 말아야 합니다.
          ...
          break;
    }
}

(주의) 많은 개발자들이 다음과 같이 구현하는것을 볼 수 있는데 절대로 다음과 같은 처리를 하지 않기를 바랍니다.

case SKPaymentTransactionStatePurchased:
  // Get the local state for this transaction
  SKPayment *payment = myCachedPayments[transaction.payment.productIdentifier];
  if (!payment)
  {
    // 이 트랜젝션이 왜 여기로 들어오는지 모르겠다!
    // 무시하자 ㅎㅎ
    continue;
  }

iOS 앱내 결제를 진행중에 수신받는 transaction 정보는 완벽하게 신뢰할 수 있는 데이터입니다. 그러므로 자체적으로 결제정보를 캐시하거나 저장하는것은 불필요합니다. 위의 코드는 앱이 크래시 되어 강제 종료되거나 결제 중간에 방해가 발생하거나 결제가 정상적으로 시작되지 않았을 경우 myCachedPayments 의 캐시 데이터의 신뢰성을 보장할 수 없습니다. 자체적으로 결제 정보를 추적하지 말고 transaction 객체를 신뢰하여 처리해 주시기 바랍니다.

상품 지급&트랜젝션 종료

정상적으로 SKPaymentTransactionStatePurchased 상태로 들어왔다면 구매한 상품의 잠금이 해제되거나 사용자에게 지급되어야 합니다. 이 과정을 수행하기 이전에 구매 영수증이 유효한지 검증하는것이 필요합니다. 이러한 영수증 검증에는 디바이스 자체의 검증을 수행하는 방법과 서버간 통신 API를 활용한 검증 방법이 있습니다.

디바이즈 자체 검증의 경우 앱내에 이미 보유중인 컨텐츠나 기능의 잠금을 해제하는 용도로 사용하기에 적합합니다. 서버간 API를 통한 검증은 접근이 제한적인 컨텐츠를 접근하여 아이템을 지급하거나 하는 용도에 적합합니다. 아마 한국의 대부분의 게임업체들의 중간화폐(Hard Currency)들은 이러한 서버간 검증을 통해 아이템을 서버에서 직접 지급하는 방식을 사용하고 있습니다.

이런 방식을 통해 아이템의 지급 작업이 끝나게 되면 앱스토어측에 이 트랜젝션의 과정이 완전히 끝났음을 다음의 API를 호출하여 알려주어야 합니다.

- (void)finishTransaction:(SKPaymentTransaction *)transaction

위의 API를 호출하게 되면 결제과정을 관리하고 있는 페이먼트큐에서 삭제가 되며 완전히 결제가 종료되게 됩니다. 여기서 중요한점은 결제가 성공하던, 실패하던 반드시 위의 코드를 실행해 주어야 한다는것입니다. 위의 호출을 하지 않을 경우 앱이 실행 될때나 혹은 앱이 실행중인 다른 상황에서 또다시 앱의 결제를 진행하려 할것이고 이는 앱의 실행을 원할하지 않은 것으로 보여지게 만들 문제의 소지가 있습니다.

(주의) 서버간 API를 디바이스에서 직접 호출하지 마십시오. 또한 iOS 6 SDK에서 제공하였던 영수증 검증 API는 Deprecated 되었습니다.

(팁) + (BOOL)canMakePayments API를 사용하여 사용자가 앱내 결제가 사용가능한지 아닌지 여부를 확인할 수 있습니다. 당신의 게임을 열심히 플레이 하여 결제를 할 정도로 빠져들었을 때 구매 시도시에 앱내 결제가 비활성화 되어있다고 오류가 발생하는것은 좋지 않은 사용자 경험이 될것입니다. 미리 디바이스에서 앱내 결제를 비활성화 한 사용자인지 여부를 판단하여 활성화 할것을 알려주는 형태로 구현하는 것이 좋을것입니다.

참고 : WWDC 2014 Videos

Xcode 6 Playground 사용하기

swift-screenshot

Xcode 6에서 새로 생긴 기능으로 Playground 라는것이 있습니다. Playground 에서 Swift 코드를 인터렉티브한 환경에서 테스트할 수 있습니다.  코드들을 컴파일 하거나 컴파일이 완료된 프로젝트를 실행하지 않고 Playground에서 당신의 실험적인 코드들의 결과를 빠르게 확인할 수 있습니다.

Playground 둘러보기

xcode_playground_01

Xcode를 실행했을 때 “Get started with a playground”를 선택하여 곧바로 Playground 프로젝트를 실행할 수 있습니다. 또는 File > New > Playground 를 선택하여 시작할 수 있습니다.

xcode_playground_03

.playground라는 확장자를 가진 파일이 생성되며 위와 같이 결과가 바로 출력되는 사이드바를 가진 창을 확인할 수 있습니다. Playground에서의 Xcode는 당신이 코드의 한 문장이 끝나거나 타이핑을 잠시 멈췄을 때 코드의 결과를 소스 에디터 오른편에 위치한 사이드바에 출력합니다.

xcode_playground_04

이제 몇가지 테스트용 코드를 작성하여 보았습니다. 오른쪽 사이드바를 보면 작성한 함수가 호출될때 내부적으로 어떻게 값이 변하는지 함수의 결과값이 무엇인지 변수의 값이 무엇인지 순환문이 몇번 반복되는지 여부를 확인할 수 있습니다.

xcode_playground_05

의도적이지만 잘못된 코드를 한번 작성해보았습니다. 이미 정의된 j를 재정의 해보려고 하였더니 곧바로 에러가 출력되는것을 확인할 수 있습니다. Xcode는 친절하게도 당신이 작성중인 코드의 잘못된 부분을 빨간색 느낌표 심볼을 사용하여 즉시 알려줍니다. 저 심볼을 클릭하게 되면 무엇을 실수하였는지 상세한 설명을 볼 수 있습니다. 실수한 부분을 알맞게 수정하거나 삭제하면 Xcode는 코드의 정상적인 실행 결과를 오른쪽 사이드바에 다시 표시해 줍니다.

xcode_playground_06

코드의 중간에 변수의 값을 확인하고 싶다면 별도의 라인에 변수명을 적기만 하면 됩니다. 위의 예시를 보면 width와 height 변수를 각각 별도의 라인에 적어줌으로써 사이드바에 결과값을 확인할 수 있습니다.

xcode_playground_07

사이드바의 순환문의 결과부분에 마우스 커서를 올려보면 위와 같은 동그라미 아이콘이 보여집니다. Value History Button이라고 하는데요, 이것을 눌러 오른쪽과 같은 값의 변화를 그래프로 확인할 수 있습니다. 심지어 소요된 시간을 확인할 수 있어 시간이 많이 소요되는 순환문의 경우 이 그래프를 확인하며 최적화 작업을 할 수 있습니다.

xcode_playground_08

이 그래프의 X축은 시간의 변화를 보여주며 Y축은 값의 변화를 보여줍니다. 위의 스크린샷과 같이 특정 지점을 마우스로 클릭하면 그 지점의 값을 확인할 수 있습니다.

xcode_playground_09

오른쪽 하단에 보면 Timeline 이라고 불리는 막대바가 있습니다. 이 바를 움직여보시면 하나의 순환문이 아닌 전체 코드들의 실행 순서를 차례대로 돌려볼 수 있습니다. 실제로 오래걸리는 순환문을 만들어 보면 그것이 실행되면서 그래프를 그리는 모습을 눈으로 관찰하실 수 있습니다.

Timeline의 막대바 옆에는 얼마나 오래동안 이 그래프가 만들어지는 과정을 보여줄것인가 제한을 설정할 수 있습니다. 레퍼런스를 읽어봤을때는 동작이 제한 시간에 걸리면 멈추는것으로 나와있는데요 테스트해본 결과 멈추지 않고 모두 실행되어 버리는것을 확인하였습니다. 좀더 확인을 해봐야 할것 같네요.

UIKit을 활용한 테스트 코드 작성하기

기본 환경에서 Playground를 사용중이라면 UILabel이라던가 UIView와 같은 UIKit에 포함된 컨트롤러를 이용한 테스트 코드를 사용할 수 없는것을 확인할 수 있었습니다. UIKit을 Import하면 당연히 될것이라 생각했는데 안되더군요. 다음과 같은 설정을 해주시면 간단히 가능하게 됩니다.

xcode_playground_10

View > Utilities > Show File Inspector 에 진입해 보면 Playground Settings가 있습니다. 여기에 Platform 설정을 iOS로 바꿔주시면 됩니다. 이제부턴 UIKit을 사용할 수 있게 되었습니다.

xcode_playground_11

이거 정말 멋지다고 생각하는데요. 현재 만들어지는 있는 각각의 컨트롤러에 대해 사이드뷰에서 눈 모양의 아이콘을 클릭하면 현재 모습을 확인할 수 있습니다. 특정 UI를 만들겠다면 정말 손쉽게 구현해보고 테스트 해볼 수 있을것 같습니다.

참고자료 : Exploring and Evaluating Swift Code in a Playground