본문 바로가기

모바일(Mobile)/안드로이드(Android)

[TIP] 안드로이드 컴포즈 환경에서 토스 결제 API 사용하기

💡 본 포스팅은 안드로이드 프로젝트 內 Jetpack Compose를 사용하는 환경에서 
XML 기반 액티비티로 결제 기능을 구현하였습니다. 즉, 컴포즈로 결제 기능을 직접 구현한 것은 아닙니다. 😀

 

개요

컴포즈 환경에서 앱 개발하던 중 앱 내 결제 기능이 필요해서 toss 의 api를 사용해서 결제 기능을 구현했습니다.

 

https://docs.tosspayments.com/reference/widget-android

 

결제위젯 Android SDK | 토스페이먼츠 개발자센터

결제위젯 Android SDK를 추가하고 메서드를 사용하는 방법을 알아봅니다.

docs.tosspayments.com

 

이때, 컴포즈로 직접 컴포저블을 만들어서 대응하는 방법도 있겠지만, 일단 저는 초보(?)고 잘 모르겠어서  컴포저블과 xml 기반의 레거시 액티비티가 함께 사용 가능하다면, 토스 공식 사이트 샘플 코드를 그대로 쓰고 싶었습니다.

 

그런데 역시 갓 구글(Google)... 컴포저블을 쓴다고 xml 기반의 액티비티를 사용하지 못하게 막지 않았고 같이 사용 가능하다는 것을 알 수 있었습니다.


다만, 이번에 구글링 하다보니 컴포즈 환경에서 toss api를 사용해서  결제 창을 구현하는 방법에 대한 레퍼런스가 적은 것 같아서, 제가 삽질하면서 시도 해본 한 가지 방법을 소개해드리고자 합니다. 

 



step 0. 토스 페이먼츠 sdk 의존성을 추가하고, 뷰바인딩 옵션을 체크합니다.

 

저는 이번에 앱 개발 시 Groovy가 아닌 KTS를 사용했기 때문에 문법이 살짝 다릅니다.
그래도 문법만 다른 거라서 다른 부분만 검색해서 찾아보면 groovy를 쓰는 다른 프로젝트에서도 충분히 적용 가능하다고 생각합니다 .  😋

 

아래와 같이 앱 수준의 build.gradle.kts에 의존성을 추가하고 뷰바인딩 옵션을 허용해줍니다.

(...)

 buildFeatures {
        compose = true
        buildConfig = true
        viewBinding = true
}

(...)

dependencies {

    (...)

    val toss_version = "0.1.11"

    // toss payments sdk
    implementation ("com.github.tosspayments:payment-sdk-android:$toss_version")

}


kts의 가장 큰 강점은 코틀린 문법으로 build 파일 건들 수 있다는 것 같아요!

근데 대부분 빌드 관련 파일은 개발자 문서에서 하란대로(?) 하니깐 엄청난 편익이 있는진 잘 모르겠지만 무튼 조금더 개발자스러운거 같네요.


step 1. 토스 페이먼츠를 위한 별도의 액티비티를 생성해주세요.

 

필자는 이런식으로 별도의 패키지로 분류해서 액티비티를 분류해 두었어요.

 

저만의 가독성을 높이기 위한 방법이긴 한데,  다른 컴포즈랑 섞인다 생각하면, 아찔하네요... 😅

 

class PaymentsActivity : AppCompatActivity() {

    val binding by lazy {ActivityPaymentsBinding.inflate(layoutInflater)}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_payments)
        setContentView(binding.root)
    }
}

 

처음에 빌드 파일 속성을 변경해주고, Android Studio를 통해 액티비티를 생성했다면 위와 같이 코드를 작성할 수 있어요!

물론, binding 변수랑  setContentView(binding.root) 는 추가로 작성해주셔야 하는데

이는 뷰바인딩을 위한 방법이니, 다른 방법이 더 편하시면 그걸로 하셔도 무방할 거 같습니다 :)

 

 

step 2. 토스 페이먼츠 액티비티를 위하여 step1.에서 함께 생성한 액티비티의 xml 을 아래와 같이 작성해주세요.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activity.PaymentsActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_editor_absoluteX="0dp"
        app:layout_editor_absoluteY="0dp"
        android:orientation="vertical">
        <com.tosspayments.paymentsdk.view.PaymentMethod
            android:id="@+id/payment_widget"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            />
        <com.tosspayments.paymentsdk.view.Agreement
            android:id="@+id/agreement_widget"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            />

        <Button
            android:id="@+id/pay_button"
            android:layout_width="match_parent"
            android:layout_height="64dp"
            android:text="결제하기"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.738" />
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

 


step 3. step1.에서 만든 액티비티로 간 뒤에 아래와 같이 코드를 작성해줍니다.

class PaymentsActivity : AppCompatActivity() {

    val binding by lazy {ActivityPaymentsBinding.inflate(layoutInflater)}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_payments)
        setContentView(binding.root)

        val clientKey = "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq"
        val secretKey = "test_sk_zXLkKEypNArWmo50nX3lmeaxYG5R"

        // 페이먼츠 위젯에 필요한 기본 속성에 대해 세팅해준다!
        val paymentWidget = PaymentWidget(
            activity = this@PaymentsActivity,
            clientKey = clientKey,
            customerKey = secretKey,
        )

        // 앱 화면에 결제창에 완전히 로딩이 되면 아래의 메세지가 로그에 찍힌다. 꼭 확인 해볼것!
        val paymentMethodWidgetStatusListener = object : PaymentWidgetStatusListener {
            override fun onLoad() {
                val message = "결제위젯 렌더링 완료"
                Log.d("PaymentWidgetStatusListener", message)
            }
        }

        // 페이먼츠 위젯에 결제 방법, 금액 등에 대해 설정할 부분이다.
        // 지금은 샘플 코드라 대부분의 옵션이 생략되어 있다.
        paymentWidget.run {
            renderPaymentMethods(
                method = binding.paymentWidget,
                amount = PaymentMethod.Rendering.Amount(10000),
                paymentWidgetStatusListener = paymentMethodWidgetStatusListener
            )

            renderAgreement(binding.agreementWidget)
        }
    }
}

 

위 코드의 10번, 11번 줄에 작성된 key 값에 대한 출처는 토스의 개발자 블로그에서 안내한대로 작성한 것이에요!

 

나중에 toss api에 별도로 key를 발급 받으시면 그 키로 변경한 후 제대로 결제하는 상점과 고객에 대한 정보를 설정해주셔야 하는데,

 

본 포스팅은 일단 앱 내에서 결제 창을 띄우는 것을 목표로 하니 가이드 문서에서 알려준 데로 테스트 키로 세팅을 했습니다 😋

 

위와 같이 작성하면, 결제 창은 보이는데 결제 과정이 진행되지 않을거에요..!

그러므로, 아래와 같이 step2.에서 별도로 작성해둔 Button 뷰에 이벤트 리스너를 걸어서 결제 과정이 진행되도록 할게요!

 

        // 결제버튼을 누르면 다음 단계로 넘어가게 하는 로직.
        // 이 부분은 토스가 제공하지 않으므로 스스로 xml에 버튼 추가해서 이벤트 리스너 셋팅해줘야 한다.
        binding.payButton.setOnClickListener {
            paymentWidget.requestPayment(
                paymentInfo = PaymentMethod.PaymentInfo(orderId = "wBWO9RJXO0UYqJMV4er8J", orderName = "orderName"),
                paymentCallback = object : PaymentCallback {
                    // 결제 프로세스에 대한 콜백 함수이다.
                    override fun onPaymentSuccess(success: TossPaymentResult.Success) {
                        Log.i("success:::", success.paymentKey)
                        Log.i("success:::", success.orderId)
                        Log.i("success:::", success.amount.toString())
                    }

                    override fun onPaymentFailed(fail: TossPaymentResult.Fail) {
                        Log.e("fail:::",fail.errorMessage)
                    }
                }
            )
        }

 

전체 소스 코드

class PaymentsActivity : AppCompatActivity() {

    val binding by lazy {ActivityPaymentsBinding.inflate(layoutInflater)}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_payments)
        setContentView(binding.root)

        val clientKey = "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq"
        val secretKey = "test_sk_zXLkKEypNArWmo50nX3lmeaxYG5R"

        // 페이먼츠 위젯에 필요한 기본 속성에 대해 세팅해준다!
        val paymentWidget = PaymentWidget(
            activity = this@PaymentsActivity,
            clientKey = clientKey,
            customerKey = secretKey,
        )

        // 앱 화면에 결제창에 완전히 로딩이 되면 아래의 메세지가 로그에 찍힌다. 꼭 확인 해볼것!
        val paymentMethodWidgetStatusListener = object : PaymentWidgetStatusListener {
            override fun onLoad() {
                val message = "결제위젯 렌더링 완료"
                Log.d("PaymentWidgetStatusListener", message)
            }
        }

        // 페이먼츠 위젯에 결제 방법, 금액 등에 대해 설정할 부분이다.
        // 지금은 샘플 코드라 대부분의 옵션이 생략되어 있다.
        paymentWidget.run {
            renderPaymentMethods(
                method = binding.paymentWidget,
                amount = PaymentMethod.Rendering.Amount(10000),
                paymentWidgetStatusListener = paymentMethodWidgetStatusListener
            )

            renderAgreement(binding.agreementWidget)
        }

        // 결제버튼을 누르면 다음 단계로 넘어가게 하는 로직.
        // 이 부분은 토스가 제공하지 않으므로 스스로 xml에 버튼 추가해서 이벤트 리스너 셋팅해줘야 한다.
        binding.payButton.setOnClickListener {
            paymentWidget.requestPayment(
                paymentInfo = PaymentMethod.PaymentInfo(orderId = "wBWO9RJXO0UYqJMV4er8J", orderName = "orderName"),
                paymentCallback = object : PaymentCallback {
                    // 결제 프로세스에 대한 콜백 함수이다.
                    override fun onPaymentSuccess(success: TossPaymentResult.Success) {
                        Log.i("success:::", success.paymentKey)
                        Log.i("success:::", success.orderId)
                        Log.i("success:::", success.amount.toString())
                    }

                    override fun onPaymentFailed(fail: TossPaymentResult.Fail) {
                        Log.e("fail:::",fail.errorMessage)
                    }
                }
            )
        }
    }
}

 



step 4. AndroidManifest.xml에서 등록된 액티비티에 대해서 테마설정을 아래와 같이 해줍니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    (...)

    <application 
    	(...)
    	>
        <activity
            android:theme="@style/Theme.MyProject"
            android:name=".activity.PaymentsActivity"
            android:exported="true" />
            
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.MyProject">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


여기서 AndroidManifest.xml 에서 세팅한 테마 xml 파일을 잘 체크해봐야 되는게...
여기서 뻘짓해가지고 왜안되지 1시간동안 삽질했답니다. 😅

 

주의할 점이 안드로이드에서 xml -> compose로 바뀌면서  values 패키지 아래에 themes.xml이라는 하나의 파일로 합쳐졌어요.

 

그래서 처음에 이것저것 속성을 건드리다보면, theme 속성이 Theme.AppCompat 어쩌구가 아닌 테마의 속성을 사용할 수 있는데요.

 

하지만 그렇게 되면 지금 별도로 만든 액티비티 파일에서 테마속성이 안맞아가지고 런타임시 강제종료되는 '런타임 에러'가 발생할 수 있더라구요.. 😂

제가 직접 보게된 오류 로그는 아래와 같았어요..! 

FATAL EXCEPTION: main
Process: com.artist.wea, PID: 15779
java.lang.RuntimeException: Unable to start activity ComponentInfo{
com.artist.MyProject/com.tosspayments.paymentsdk.activity.TossPaymentActivity}
: java.lang.IllegalStateException: 
You need to use a Theme.AppCompat theme (or descendant) with this activity.
...
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:197)
at com.tosspayments.paymentsdk.activity.TossPaymentActivity.onCreate(TossPaymentActivity.kt:77)
...


원인은 compose와 xml 을 함께 사용해서 앱을 구현하다보니,

 

토스 sdk 에서 안드로이드의 view를 생성할 때 프로젝트에서 설정해둔 테마 설정을 따르는데, 그 테마가 Theme.Appcompat .xxx  가 아니라면, 오류가 발생하고 런타임 시에 뷰를 생성하다가 액티비티가 종료가 되는 현상을 발견했습니다.

 

 

v1.0. 런타임 오류 화면

 

기존에 컴포즈에 세팅한 테마 설정 자체에는 문제가 없었다보니, 앱 자체가 종료되진 않아서 신기한 오류를 발견했네요..!


해결 방법은 아래와 같이 theme.xml 파일을 직접 수정해주었습니다.

그러므로 토스 결제창이 뭔가 좀 이상하다면 theme.xml 까지 한 번더 체크해보는 것을 추천드립니다 :)

 

fig1.0. theme.xml 위치

 

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme.MyProject" parent="Theme.AppCompat.DayNight">
        <item name="colorPrimary">@color/white</item>
        <item name="colorPrimaryVariant">@color/mono100</item>
        <item name="colorOnPrimary">@color/white</item>
        <item name="background">@color/white</item>
        <item name="backgroundColor">@color/white</item>        
    </style>
</resources>


여기까지 잘 점검하고 따라왔다면, 아래와 같이 결제 창이 잘 넘어가는 것을 확인할 수 있습니다.

 

사실 결제에서 중요한 대부분의 과정들은 토스에서 잘 구현해두었기 때문에

별도로 신경쓸 부분은 없고 그게 참 좋은 부분인거 같네요.

 

v1.1. 결제창 완성

 





참고한 게시글

https://velog.io/@tosspayments/Android-%EC%95%B1%EC%97%90%EC%84%9C-%EA%B2%B0%EC%A0%9C-%EC%A3%BC%EB%AC%B8%EC%84%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95
https://docs.tosspayments.com/reference/widget-android

 

Android 앱에서 결제 주문서 만드는 방법

Android의 4대 컴포넌트 중 하나인 액티비티(Activity)가 뭔지 알아보고 토스페이먼츠 결제위젯으로 간단한 결제 주문서 화면을 만들어볼게요.

velog.io

https://docs.tosspayments.com/reference/widget-android

 

결제위젯 Android SDK | 토스페이먼츠 개발자센터

결제위젯 Android SDK를 추가하고 메서드를 사용하는 방법을 알아봅니다.

docs.tosspayments.com