이 포스트는 토스비즈니스피드에서도 읽을 수 있습니다.
토스페이먼츠는 Android, iOS SDK를 제공하고 있는데요. 만약 Android 개발이 처음이라면 SDK 연동이 어려울 수 있어요. 이번 포스트에는 Android의 4대 컴포넌트 중 하나인 액티비티(Activity)가 뭔지 알아보고 토스페이먼츠 Android 결제위젯 SDK로 간단한 결제 주문서 화면을 만들어볼게요.
앱은 보통 홈 화면, 로그인 화면, 결제 화면 등 여러 화면으로 구성되어 있는데요. Android 액티비티(Activity)는 앱의 화면이에요. 앱을 켰을 때 바로 보이는 홈 화면은 보통 ‘메인 액티비티’라고 불러요. 결제 화면은 ‘결제 액티비티’로 만들 수 있는 거죠. 일반적으로 액티비티는 웹페이지와 같이 화면을 채우는 UI 창이지만, 필요에 따라 다른 창 위에 작게 띄울 수도 있어요.
액티비티는 ‘Kotlin(Java) 클래스 파일’과 ‘레이아웃 XML 파일’로 구성되어 있어요. XML 파일에서는 액티비티의 UI를 자유롭게 만들 수 있어요. View 클래스와 Button 또는 TextView와 같은 서브클래스를 XML 어휘로 제공하고 있기 때문이죠. 액티비티를 생성하고 View에 동작을 추가하는 코드는 클래스 파일에 정의해야 돼요. 예를 들어, XML 파일에 버튼을 만들었다고 가정할게요. 버튼을 눌렀을 때 새로운 액티비티를 시작하는 로직은 클래스 파일에 정의해야 돼요.
▪️ 레이아웃 코드와 동작 로직 코드를 따로 정의하면 관심사가 분리돼요. XML에서 레이아웃을 쉽게 정의할 수 있고, 유연하게 앱 UI를 빌드할 수 있다는 장점이 있어요.액티비티의 Kotlin 클래스 파일과 레이아웃 XML 파일이 어떻게 연결되는지 코드로 더 자세히 살펴볼게요. Android Studio에서 New > Activity를 만들면 클래스 파일과 XML 파일이 자동으로 생성돼요. 클래스 파일은 MainActivity.kt
와 같이, XML 파일은 res/layout
밑에 activity_main.xml
와 같이 추가돼요.
이 두 개의 파일은 어떻게 서로 연결될까요? onCreate()
메서드로 액티비티를 생성한 다음, setContentView()
의 파라미터로 XML 파일을 넣어서 액티비티의 레이아웃 정의해요.
// MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
마지막으로 AndroidManifst.xml
파일에 액티비티를 추가하면 앱에서 액티비티를 정상적으로 사용할 수 있어요. <application>
태그 안에 생성한 액티비티를 등록해요.
// AndroidManifst.xml
<manifest ... >
<application ... >
<activity android:name="com.example.myapplication.MainActivity" ... >
</activity>
</application>
</manifest>
Android Studio에서 새로운 액티비티를 생성하면, 위 작업은 자동으로 돼요. 하지만 수동으로 클래스 파일을 추가하고, 레이아웃 XML 파일을 만들었다면 직접 연결하는 작업을 해주세요.
액티비티의 UI, 클래스 코드가 어떻게 연결되고 구성되는지 알아봤는데요. 실제로 View와 클래스 코드가 상호 작용하기 위해서는 어떻게 해야 될까요?
클래스 파일에서 findViewById()
로 View를 지정하고 상호 작용하는 코드를 작성하면 돼요. 하지만 View에 동작을 추가하고 싶을 때마다 함수를 호출해야 되면 너무 번거롭겠죠. 이런 문제는 View Binding 클래스로 해결할 수 있어요. Binding 인스턴스를 생성하면, XML 파일에 있는 모든 View를 ID로 간편하게 참조할 수 있게 돼요.
먼저 build.gradle
파일에 View Binding을 허용하세요. 다음, 액티비티 파일에 Binding 인스턴스를 생성하고, setContentView()
에 XML 파일 대신 Binding 루트를 넘기세요. 이제 모든 View를 클래스 파일에서 binding.{id}
로 참조할 수 있어요.
// build.gradle
android {
viewBinding {
enabled = true
}
}
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
View Binding은 액티비티뿐만 아니라 프래그먼트(Fragment)도 사용할 수 있어요. 프래그먼트에서 View Binding 사용하는 방법은 Android 공식 가이드를 참고하세요.
액티비티와 View Binding을 결제 페이지를 만드는 실제 코드에서 사용해보면 이해하기 더 쉬울 텐데요. 토스페이먼츠 결제위젯 SDK로 결제 액티비티를 만들어볼게요. 결제위젯은 토스페이먼츠가 수많은 상점을 분석하여 만든 최적의 주문서 결제 UI를 노코드로 운영할 수 있는 제품이에요.
프로젝트 설정
Android Studio에서 Empty Activity 프로젝트를 생성하세요.
settings.gradle
에 maven { url "https://jitpack.io" }
를 추가하세요.
dependencyResolutionManagement{
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories{
google()
mavenCentral()
maven{ url "https://jitpack.io" }
}
}
build.gradle(app)
에 View Binding을 허용해주고 토스페이먼츠 SDK를 추가하세요.
android {
...
viewBinding {
enabled = true
}
}
dependencies{
...
implementation 'com.github.tosspayments:payment-sdk-android:0.1.9'
}
레이아웃 만들기
View를 이용해서 XML 파일에 결제 주문서 화면의 레이아웃을 만들어줄게요.
PaymentMethod
View를 추가하세요.Agreement
View를 추가하세요.// activity_main.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"
android:background="@color/white">
<androidx.core.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/cta_container"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.tosspayments.paymentsdk.view.PaymentMethod
android:id="@+id/payment_method_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" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cta_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:padding="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/request_payment_cta"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="56dp"
android:text="결제하기"
android:textColor="@color/black"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
결제위젯 연동
화면 구성은 완성했는데요. 지금 앱을 띄우면 화면에 아무것도 나오지 않아요. 만들어놓은 View에 결제위젯, 이용약관 위젯을 띄우고, 결제 버튼에 결제 요청 로직을 클래스 코드에 추가해줘야 돼요.
시작하기 전에, View Binding을 설정해주세요.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
이제 결제위젯 생성자에 현재 액티비티, 클라이언트 키, 고객 키를 넣어 결제위젯 인스턴스를 만들어주세요.
clientKey
: 토스페이먼츠에서 발급하는 연동 키입니다. API 키 페이지에서 테스트 클라이언트 키값을 사용하세요. 아래 코드에 있는 클라이언트 키를 사용할 수도 있어요.customerKey
: 고객 ID입니다. 다른 사용자가 이 값을 알게 되면 악의적으로 사용할 수 있어 자동 증가하는 숫자는 안전하지 않습니다. UUID와 같이 충분히 무작위적인 고유 값으로 생성해주세요. 영문 대소문자, 숫자, 특수문자 -
, _
, =
, .
, @
를 최소 1개 이상 포함한 최소 2자 이상 최대 300자 이하의 문자열이어야 합니다.val paymentWidget = PaymentWidget(
activity = this@MainActivity,
clientKey = "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq",
customerKey = "7CP_K-knksQZ966GZAfhm",
)
생성된 결제위젯 인스턴스로 결제위젯, 이용약관 위젯을 View에 렌더링할게요. XML 파일에 만들어 놓은 View는 binding.{id}
형태로 불러오면 돼요. 아래 코드까지 추가하고 앱을 실행하면 이제 결제 방법과 이용약관이 보일 거예요.
paymentWidget.run {
renderPaymentMethods(
binding.paymentMethodWidget,
PaymentMethod.Rendering.Amount(10000)
)
renderAgreement(binding.agreementWidget)
}
결제위젯 렌더링이 완료된 시점을 알고 싶다면 paymentWidgetStatusListener
을 사용할 수 있어요. 결제위젯의 렌더링 상태를 감지하는 리스너인데요. 리스너를 만들고, renderPaymentMethods()
파라미터로 사용하면 돼요. 아래 코드는 간단히 로그에 “결제위젯 렌더링 완료” 메시지를 찍고 있어요. 결제위젯 렌더링이 완료됐을 때 실행하고 싶은 코드를 추가해주세요.
val paymentMethodWidgetStatusListener = object : PaymentWidgetStatusListener{
override fun onLoad() {
val message = "결제위젯 렌더링 완료"
Log.d("PaymentWidgetStatusListener", message)
}
}
paymentWidget.run {
renderPaymentMethods(
method = binding.paymentMethodWidget,
amount = PaymentMethod.Rendering.Amount(10000),
paymentWidgetStatusListener = paymentMethodWidgetStatusListener
)
}
하지만 아직 ‘결제하기’ 버튼을 눌러도 아무 일도 일어나지 않아요. 이제는 버튼을 누르면 결제를 요청하는 코드를 추가할게요.
고객이 버튼을 클릭할 때 requestPayment()
를 호출할게요. orderId
, orderName
등 필요한 파라미터를 모두 추가한 다음 결제 요청의 결과를 받는 콜백 함수도 추가해요. 아래 예제 코드의 콜백 함수는 결제 요청 성공/실패 시 간단히 로그를 남기고 있는데요. 실제로 결제를 완성하고 싶다면 성공 콜백 함수에 결제 승인을 요청해주세요.
binding.requestPaymentCta.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)
}
}
)
}
orderId
: 주문을 구분하는 ID입니다. 충분히 무작위한 값을 생성해서 각 주문마다 고유한 값을 넣어주세요. 영문 대소문자, 숫자, 특수문자 -
, _
, =
로 이루어진 6자 이상 64자 이하의 문자열이어야 합니다.orderName
: 주문명입니다. 예를 들면 생수 외 1건
같은 형식입니다. 최대 길이는 100자입니다.코드를 모두 추가하고 앱을 다시 실행하세요. 이제 ‘결제하기’ 버튼을 누르면 선택한 결제수단의 결제창이 나오는 걸 확인할 수 있어요. 로그에도 성공 데이터가 잘 찍히는지 확인하세요.
이제 결제 요청까지 완료했는데요. TossPaymentResult.Success
에 돌아온 정보로 결제 승인에 성공해야 결제가 완료됩니다. 결제 화면에서 각 은행・카드사 앱으로 잘 이동하는지도 확인을 해야 되고요. 만약에 금액이 쿠폰 등 이유로 바뀐다면 위젯 UI에 금액도 업데이트해줘야 되죠. 자세한 내용은 결제위젯 연동 가이드, 결제위젯 Android SDK 레퍼런스를 참고하세요.
// MainActivity.kt
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.example.myapplication.databinding.ActivityMainBinding
import com.tosspayments.paymentsdk.PaymentWidget
import com.tosspayments.paymentsdk.model.PaymentCallback
import com.tosspayments.paymentsdk.model.PaymentWidgetStatusListener
import com.tosspayments.paymentsdk.model.TossPaymentResult
import com.tosspayments.paymentsdk.view.PaymentMethod
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val paymentWidget = PaymentWidget(
activity = this@MainActivity,
clientKey = "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq",
customerKey = "7CP_K-knksQZ966GZAfhm",
)
val paymentMethodWidgetStatusListener = object : PaymentWidgetStatusListener{
override fun onLoad() {
val message = "결제위젯 렌더링 완료"
Log.d("PaymentWidgetStatusListener", message)
}
}
paymentWidget.run {
renderPaymentMethods(
method = binding.paymentMethodWidget,
amount = PaymentMethod.Rendering.Amount(10000),
paymentWidgetStatusListener = paymentMethodWidgetStatusListener
)
renderAgreement(binding.agreementWidget)
}
binding.requestPaymentCta.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)
}
}
)
}
}
}
함께 읽으면 좋을 콘텐츠
토스페이먼츠 Twitter를 팔로우하시면 더욱 빠르게 블로그 업데이트 소식을 만나보실 수 있어요.
뛰어난 글이네요, 감사합니다.