✨ Read how Fotorama reduced app’s subscription refund rate by 40% with Refund Saver

Android 인앱 구매, 1부: 환경 설정 및 프로젝트에 추가

Vlad Guriev

Updated: 3월 20, 2023

Content

62fdf1dbf601140886432256 jp android tutorial 1 configuration 1 4

인앱구매 (in-apppurchase), 특히구독 (subscription)은앱 (app)에서수익을 내는 가장 인기 있는 방법입니다.개발자는구독을 통해 콘텐츠 및 제품 개발에 투자할 수 있고,다른한편으로는 사용자가 일반적으로 더 높은 품질의 앱을얻는 데 도움이 됩니다.인앱구매에는 30%수수료가부과되지만,사용자가1년이상 구독한 경우 또는 앱이 연간$1M미만의수익을 올리는 경우,수수료는15%입니다.

이문서에서는 다음에 대한 방법을 설명합니다.

  • Google Play Console에서 제품 생성하기,
  • 구독 구성: 기간, 가격, 체험 지정하기,
  • 앱에 제품 목록 추가하기.

구독생성하기

시작하기전에 다음을 확인하세요.

  1. Google Play 개발자 계정을 갖고 있습니다.
  2. 모든 계약에 서명했고 작업을 시작할 준비가 되었습니다.

이제본격적으로 첫 번째 제품을 만들어 봅시다.

귀하의개발자계정으로전환하고 앱을 선택합니다.

60fef7a69f036b3bfc996258 jsaoezesquwst5gknnib6kc3xqckd8yldedvgpfkaagrqbg9xksbckpwo g7ypda7kelwc zzkmwkt2powjgjwok3yphfxvmaquf6fhiqrqtyz06nuu5yfdyta502n1saakxczfz 4

그런다음,왼쪽메뉴에서 제품 섹션을 찾아 Subscriptions를선택하고 Createa Subscription을클릭합니다.

60fef7a7b3b5c7c37e286978 qy go34alibtdmnfdkaii3wg1fcjv5bexstyoefeazq 2oagsnx37gndqykcx6rcwcyu7ahpfzug gusku2bg0sxtq nkmglkw5uuvlabnnlnmp7j2ljfc9 nkzhyhbhegp1nl8r 4

그러면구독 환경 설정이 나타납니다.다음은몇 가지 중요한 사항입니다.  

60fef7a7d069f50dac0202a5 edgfvm0dhtqew hin9r1uqwvyzuzna1nw4auk hsl8kvgoc2uev8ulj5yhinnvdwv6xau1c1kzy2vd0fmpc8ezyurr3vtl 1qbj19ewbfwqspewed3d echiz1gjqfrfpeiwm0yb 4
  1. 앱에서 사용될 아이디를 생성합니다. 아이디에 구독 기간이나 기타 유용한 정보를 추가하면 하나의 스타일로 제품을 만들 수 있고, 판매 통계를 더 쉽게 분석할 수 있습니다.
  2. 사용자가 스토어에서 보게 될 구독명입니다.  
  3. 구독 설명. 사용자도 볼 수 있습니다.
60fef7a755430242d074b04c 5mix72nt8rn7nbjrkxd7l7pfit6yuebw9f8x0bke47mx1pbvvagpf4uar w dlkpkkfl9q7udzodqctd451avnwnvqccvtmz0wwj90f8md8mbmrl464wo ljps2wriusg8ax8p1c 4

아래로스크롤하여 구독 기간을 선택합니다.우리의경우에는 일주일입니다.가격을설정합니다.

60fef7a8d11d44258c894e7d 658mke hovj itrvd6mamegcdp6wvefdt0bdamvvssnwg0cca hwzr7wcgvm9sxf7f6a5iipl2xuaxm9hwznynmah1ad4n7ttsl4uxjpt29aimx2swzisoplqaxcj5avpepiocum 4

대개기본 계정 통화로 가격을 설정하면,시스템에서자동으로 가격을 변환합니다.그러나특정 국가의 가격을 수동으로 수정할 수도 있습니다.

Google은모든 국가에 대한 세금을 표시한다는 점에 주의하십시오.다행히App StoreConnect (App Store Connect)는그렇게 하지 않습니다.

60fef7a7ebfe90328d0a6a28 3k q7t5g7luv4axvw25thwlj3aibgsnnkahdtfeleaba31hqwgvo1yin2qayyh1r97tp4p nyvavypbvrt2 9caldi51tr15xhj4tamhq6kuluec5gikmuya1irmfzibqu79mfj1 4

아래로스크롤하여 선택합니다 (필요한경우):

  1. 무료 체험 (Free trial) 기간.
  2. 출시 기념가, 즉 첫 결제 기간에 제공되는 할인 가격입니다.  
  3. 유예 기간. 사용자에게 결제 문제가 있는 경우, 며칠 동안 프리미엄 액세스를 계속 제공할 수 있습니다.
  4. 해지 후 앱이 아닌 Play 스토어에서 재구독할 수 있는 기회입니다.

Play Console과AppStore Connect의구매 프로세스 비교

iOS에서구독이 더 효과적으로 수익을 올릴 수 있는 것은사실이지만,Play Console의관리 게시판은 더 편리하고 더 잘 구성 및 현지화되어있으며,더빠르게 작동합니다.

제품생성 과정은 매우 간단합니다.여기iOS에서의제품 생성 방법에 대해 다루었습니다.

앱에제품 목록 가져오기

제품이생성되면,구매수락 및 처리를 위한 아키텍처를 작업해 보겠습니다.일반적인절차는 다음과 같습니다.  

  1. 결제 라이브러리를 추가합니다.
  2. Google Play 제품과의 상호작용을 위한 클래스를 개발합니다.
  3. 구매를 처리하는 모든 방법을 구현합니다.  
  4. 구매 서버 검증을 추가합니다.  
  5. 분석을 수집합니다.

여기에서는처음 두 가지 점을 자세히 살펴보겠습니다.

프로젝트에결제 라이브러리 추가하기:

implementation "com.android.billingclient:billing:4.0.0"

이글을 작성하는 시점에서 최신 버전은 4.0.0입니다.언제든지다른 버전으로 교체할 수 있습니다.

GooglePlay와의상호 작용 논리를 다루는 래퍼 클래스를 만들고 그안의 결제 라이브러리에서 BillingClient를초기화해 봅시다.이클래스를 BillingClientWrapper라고부르겠습니다.

이클래스는 PurchasesUpdatedListener인터페이스를구현할 것입니다.이제메소드를 재정의할 것입니다.onPurchasesUpdated(billingResult:BillingResult, purchaseList: MutableList<Purchase>?)– 구매가이루어진 직후에 필요하지만,구현과정은 다음 기사에서 설명하겠습니다.

import android.content.Context import com.android.billingclient.api.*  class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {     private val billingClient = BillingClient        .newBuilder(context)        .enablePendingPurchases()        .setListener(this)        .build()     override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {        // here come callbacks about new purchases    } }

Google은 구매에 대한 콜백의 다중 실행을 방지하기 위하여,BillingClient와GooglePlay 간에둘 이상의 활성 연결을 갖는 것을 피하기를 추천합니다.따라서싱글톤 클래스에 하나의 고유한 BillingClient만있어야 합니다.예제의클래스는 싱글톤이 아니지만,의존성주입(예를들어,Dagger또는Koin의도움으로)을이러한 방식으로 사용하여,한시점에 하나의 인스턴스만 존재하도록 허용합니다.

결제라이브러리를 통한 요청을 하려면,BillingClient는요청이 이루어지는 순간에 GooglePlay와활성 연결이 되어 있어야 하지만,연결이어느 순간 끊어질 수 있습니다.편의를위해 연결이 활성화된 경우에만 요청을 수행할 수 있는래퍼를 작성해 봅시다.

제품을얻으려면 마켓에 설정한 ID가필요합니다.그러나이것만으로는 요청에 충분하지 않고,제품유형 (구독또는 일회성 구매)도필요하기때문에,두요청의 결과를 “결합”하여일반 제품 목록을 얻을 수 있습니다.  

제품요청은 비동기식이므로,제품목록을 제공하거나 오류 모델을 반환하는 콜백이필요합니다.오류가발생하면 결제 라이브러리는BillingResponseCodes중하나뿐 아니라 debugMessage도반환합니다.콜백인터페이스와 오류 모델을 만들어 보겠습니다.

interface OnQueryProductsListener {   fun onSuccess(products: List < SkuDetails > )   fun onFailure(error: Error) }  class Error(val responseCode: Int, val debugMessage: String)

다음은특정 유형의 제품에 대한 데이터를 가져오는 비공개메소드와 두 요청의 결과를 “결합”하고사용자에게 최종 제품 목록을 제공하거나 오류 메시지를표시하는 공개 메소드에 대한 코드입니다.

fun queryProducts(listener: OnQueryProductsListener) {    val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")     queryProductsForType(        skusList,        BillingClient.SkuType.SUBS    ) { billingResult, skuDetailsList ->        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {            val products = skuDetailsList ?: mutableListOf()            queryProductsForType(                skusList,                BillingClient.SkuType.INAPP            ) { billingResult, skuDetailsList ->                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {                    products.addAll(skuDetailsList ?: listOf())                    listener.onSuccess(products)                } else {                    listener.onFailure(                        Error(billingResult.responseCode, billingResult.debugMessage)                    )                }            }        } else {            listener.onFailure(                Error(billingResult.responseCode, billingResult.debugMessage)            )        }    } }  private fun queryProductsForType(    skusList: List<String>,    @BillingClient.SkuType type: String,    listener: SkuDetailsResponseListener ) {    onConnected {        billingClient.querySkuDetailsAsync(            SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),            listener        )    } }

이렇게우리는 제품에 대한 귀중한 정보를 얻었고 (SkuDetails)여기에서현지화된 이름,가격,제품유형은 물론,구독결제 기간 및 출시 기념가와 평가판 기간 (해당사용자가 사용할 수 있는 경우)에대한 정보도 볼 수 있습니다.최종클래스는 다음과 같습니다.    

import android.content.Context import com.android.billingclient.api.* class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {    interface OnQueryProductsListener {        fun onSuccess(products: List<SkuDetails>)        fun onFailure(error: Error)    }    class Error(val responseCode: Int, val debugMessage: String)    private val billingClient = BillingClient        .newBuilder(context)        .enablePendingPurchases()        .setListener(this)        .build()    fun queryProducts(listener: OnQueryProductsListener) {        val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")        queryProductsForType(            skusList,            BillingClient.SkuType.SUBS        ) { billingResult, skuDetailsList ->            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {                val products = skuDetailsList ?: mutableListOf()                queryProductsForType(                    skusList,                    BillingClient.SkuType.INAPP                ) { billingResult, skuDetailsList ->                    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {                        products.addAll(skuDetailsList ?: listOf())                        listener.onSuccess(products)                    } else {                        listener.onFailure(                            Error(billingResult.responseCode, billingResult.debugMessage)                        )                    }                }            } else {                listener.onFailure(                    Error(billingResult.responseCode, billingResult.debugMessage)                )            }        }    }    private fun queryProductsForType(        skusList: List<String>,        @BillingClient.SkuType type: String,        listener: SkuDetailsResponseListener    ) {        onConnected {            billingClient.querySkuDetailsAsync(                SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),                listener            )        }    }    private fun onConnected(block: () -> Unit) {        billingClient.startConnection(object : BillingClientStateListener {            override fun onBillingSetupFinished(billingResult: BillingResult) {                block()            }            override fun onBillingServiceDisconnected() {                // Try to restart the connection on the next request to                // Google Play by calling the startConnection() method.            }        })    }    override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {        // here come callbacks about new purchases    } }

여기까지입니다.다음기사에서는 구매 구현(purchaseimplementation),테스트및 오류 처리에 대해 알려 드리겠습니다.

Unlock 2024 subscription secrets
Access our free 2024 in-app subscription report to view essential benchmarks and market trends.
Includes cheat sheets!
Get your free report
Unlock 2024 subscription holiday secrets