Jiwift

[iOS/Swift] Toss Payments 결제 위젯 SDK 연동하기 본문

라이브러리/TossPayments

[iOS/Swift] Toss Payments 결제 위젯 SDK 연동하기

지위프트 2023. 10. 30. 14:17
반응형

 이번 시간에는 Toss Payments의 결제 위젯 SDK를 연동하도록 하겠습니다. 사실 이미 토스 패이먼츠 개발자 블로그나 개발자 센터에 방법이 다 나와있기는 합니다. 하지만 정리하고 넘어가기 위해서 글을 작성해 보도록 하겠습니다.

 

 *진행을 하기 위해서는 토스 서버와 연결 작업이 완료된 서버가 있다는 가정하에 앱 부분만 설명하도록 하겠습니다.

 *이 글에서는 Storyboard가 아닌 코드를 통한 UI 구현으로 진행하겠습니다. 

 


 

1. 초기 선언 작업

2. 위젯 UI 보이기

3. 사용자 구매 정보 입력 및 결제 진행

4. Delegate를 통해 결과받고 처리하기 

 앱에서 결제를 처리하기 위한 단계로는 크게 4가지로 볼 수 있습니다. 결제에 필요한 초기 선언과 init 작업들을 진행하고, 위젯 UI를 출력한 후 구매자의 결제 정보를 받아 성공 여부를 Delegate로 받는 것입니다. 그리고 결과에 따라서 에러를 보여줄지 서버에 정보를 넘길지 처리를 하게 됩니다. 

 

 

해당 글은 토스 페이먼츠 벨로그에 있는 코드를 기반으로 수정하여 작성되었습니다.

 

 


 

 

 이제 위 작업을 수행하기 위한 구현을 진행하겠습니다. 아래 순서대로 라이브러리 설치와 코드 작성 및 앱 이동 설정 등을 수행해 주세요.

 

1. SDK 설치

Toss Payments SDK 설치

https://github.com/tosspayments/payment-sdk-ios.git

 제일 먼저 해주는 작업은 SDK 설치입니다. 토스에서는 CocoaPods과 SPM을 지원하고 있습니다. 저는 편의를 위해서 SPM을 사용하겠지만 각자 편한 방법으로 설치를 진행해 주세요.

 

토스페이먼츠 iOS SDK를 설치하기 전에 최소 요구 사항입니다.

  • iOS 11.0 이상
  • Swift 5.0 이상
  • Xcode 12.5.1 이상

 

2. 초기 선언

import TossPayments

 너무나 당연하겠지만 사용을 원하는 곳에서 토스 SDKimport 해주세요. 

 

// UI가 담길 공간
public lazy var scrollView = UIScrollView()
public lazy var stackView = UIStackView()

/*
 위젯 생성
 clientKey - 내 상점의 클라이언트 키 즉 운영하는 서비스에 발부된 키
 customerKey - 고객 ID, 직접 생성해야하며 유출되면 악의적으로 사용할 수 있어 UUID와 같은 충분히 무작위적인 고유 값을 추천
 */
private lazy var widget: PaymentWidget = PaymentWidget(
    clientKey: "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq",
    customerKey: "EPUx4U0_zvKaGMZkA7uF_"
)

// 결제 버튼
private lazy var button = UIButton()

  저는 결제용 ViewController를 만들어서 선언 작업을 진행합니다. 위에서부터 '결제 위젯'과 '버튼'이 담길 스크롤뷰와 스택뷰를 선언했습니다. 그리고 위젯을 초기 선언을 해주고 결제를 위한 버튼을 선언해 주었습니다. 

 

 UI가 담길 곳은 Storyboard에서 작업을 하던 어떻게 하던 자유롭게 하시면 됩니다. 저는 토스 개발 블로그에서 방식이 편해서 유지하기로 했습니다. 

 

 위젯 선언할 때는 ClientKeyCustomerKey가 필요합니다. 지금은 테스트키로 구성되어 있지만, 나중에는 실제로 발급받은 키들을 넣어주어야 합니다. 

 

 버튼은 결제를 하기 위한 용도로 선언을 해둡니다. 

 

2. UI 셋팅

// 스택 뷰와 스크롤 뷰 추가
self.view.addSubview(self.scrollView)
self.scrollView.addSubview(self.stackView)
self.scrollView.alwaysBounceVertical = true
self.scrollView.keyboardDismissMode = .onDrag

// 스택 뷰 레이아웃
self.stackView.spacing = 24
self.stackView.axis = .vertical

// 스택 뷰, 스크롤 뷰 AutoLayout을 사용하기 위함
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
self.stackView.translatesAutoresizingMaskIntoConstraints = false

// AutoLayout
NSLayoutConstraint.activate([
    self.scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
    self.scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
    self.scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
    self.scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
    self.scrollView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor),

    self.stackView.topAnchor.constraint(equalTo: self.scrollView.topAnchor),
    self.stackView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor, constant: 24),
    self.stackView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor, constant: -24),
    self.stackView.widthAnchor.constraint(equalTo: self.scrollView.widthAnchor, constant: -48),
    self.stackView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor)
])

// 버튼 추가 및 설정
self.view.addSubview(self.button)
self.button.backgroundColor = .systemBlue
self.button.setTitle("결제하기", for: .normal)
self.button.addTarget(self, action: #selector(self.requestPayment), for: .touchUpInside)

// 결제 위젯을 랜더링하는 메서드
let paymentMethods = self.widget.renderPaymentMethods(amount: PaymentMethodWidget.Amount(value: 10000))
// 이용약관을 랜더링하는 메서드
let agreement = self.widget.renderAgreement()

// 랜더링 결과들을 스택 뷰에 Add
self.stackView.addArrangedSubview(paymentMethods)
self.stackView.addArrangedSubview(agreement)
self.stackView.addArrangedSubview(self.button)

// 위젯 결과를 위한 Delegate
self.widget.delegate = self
self.widget.paymentMethodWidget?.widgetStatusDelegate = self

 코드는 길지만 UI 설정이 대부분입니다. Storyboard로 작업하신 분들은 여기서 안 쓰이는 코드들은 제거를 해주세요. 저는 테스트 코드이기 때문에 viewDidLoad에 이 코드를 모두 넣었습니다. 

 

 중간에 유심히 보아야 할 곳은 '결제 위젯을 랜더링 하는 메서드', '이용약관을 랜더링 하는 메서드'입니다. 이 메서드를 통해서 우리가 토스에서 설정한 화면이 나올 것이고 금액도 설정하게 됩니다. 그리고 위에서 선언한 버튼을 결제하기 함수와 연결해 줍니다.

 

 생성된 토스 UI들은 스택뷰에 그대로 넣어줍니다. 그리고 Delegate들을 연결해 줍니다. 위에서부터 '결제 결과를 받는 Delegate'와 '결제 위젯 상태를 확인하는 Delegate'입니다. 이를 통해서 결제 성공/실패와 랜더링 완료 여부등을 알 수 있습니다.

 

*구현하는 방법은 100% 똑같이 진행하실 필요는 없습니다. 위 작업들이 어떤 기능을 하는지 알고 순서를 지킨다면 구현하기 나름입니다. 

 

3. 결제하기

// 고객이 선택한 결제 수단의 결제창을 띄우는 메서드
@objc func requestPayment() {
    self.widget.requestPayment(
        info: DefaultWidgetPaymentInfo(
        orderId: "2VAhXURbYbiKwX5ybfrLr",
        orderName: "토스 티셔츠 외 2건")
    )
}

 '결제하기' 버튼에 연결되는 함수입니다. '결제 위젯'에서 고객이 결제 수단을 선택하고 '결제하기' 버튼을 누르면 각 결제 수단에 맞는 동작을 수행하게 됩니다. 저 같은 경우 orderIdorderName은 서버에서 받습니다.(아마 모두들 그렇지 않을까..)

 

4. TossPaymentsDelegate

extension ViewController: TossPaymentsDelegate {
    // 결제가 성공한 경우
    public func handleSuccessResult(_ success: TossPaymentsResult.Success) {
        print("결제 성공")
        print("paymentKey: \(success.paymentKey)")
        print("orderId: \(success.orderId)")
        print("amount: \(success.amount)")
    }

    // 결제가 실패한 경우
    public func handleFailResult(_ fail: TossPaymentsResult.Fail) {
        print("결제 실패")
        print("errorCode: \(fail.errorCode)")
        print("errorMessage: \(fail.errorMessage)")
        print("orderId: \(fail.orderId)")
    }
}

TossPaymentsDelegate 사용자가 결제를 성공했는지 실패했는지 Delegate를 통해서 결과를 받게 됩니다. 제가 구현한 앱에서는 결제 위젯에서 받은 정보는 서비스를 위해 구현한 서버로 보내주는 처리를 하였습니다. (서버는 paymentKey가 필요해...)

 

5. TossPaymentsWidgetStatusDelegate

extension ViewController: TossPaymentsWidgetStatusDelegate {
    // 결제위젯 렌더링이 완료되면 호출되는 메서드
    public func didReceivedLoad(_ name: String) {
        print("결제위젯 렌더링 완료 ")
    }
}

 랜더링 완료되는 결과를 확인하기 위함입니다. 저 같은 경우 여기서는 따로 처리한 것은 없습니다. 

 

 

여기까지 진행이 완료되었다면 오류 없이 '결제 위젯'이 출력될 것이고 화면 테스트가 가능합니다. 하지만 결제 과정에서 '현대카드', 'KBPay'와 같은 앱이 필요하다면 추가적인 설정이 필요합니다. 

 

 제대로 동작하기 위해서는 info.plist를 설정해주어야 하는데, 하지 않으면 앱이 설치되어 있어도 계속 앱스토어로 이동하기만 하지 결제 다음 단계로 진행되지 않는 문제가 있습니다. 

 

다른 앱으로 이동하기 위한 설정은 글이 길어지는 관계로 다음 글에서 이어서 작성하겠습니다.

   

 


 

마무리로는 결과 화면 캡처와 모든 코드를 첨부하고 마무리하겠습니다. 

 

결제 수단 선택 화면
결제하기 버튼 눌렀을 때

 

 

//
//  ViewController.swift
//  TossPaymentWidget
//
//  Created by 김지태 on 10/24/23.
//

import UIKit
import TossPayments

class ViewController: UIViewController {

    // UI가 담길 공간
    public lazy var scrollView = UIScrollView()
    public lazy var stackView = UIStackView()
    
    /*
     위젯 생성
     clientKey - 내 상점의 클라이언트 키 즉 운영하는 서비스에 발부된 키
     customerKey - 고객 ID, 직접 생성해야하며 유출되면 악의적으로 사용할 수 있어 UUID와 같은 충분히 무작위적인 고유 값을 추천
     */
    private lazy var widget: PaymentWidget = PaymentWidget(
        clientKey: "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq",
        customerKey: "EPUx4U0_zvKaGMZkA7uF_"
    )
    
    // 결제 버튼
    private lazy var button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // 스택 뷰와 스크롤 뷰 추가
        self.view.addSubview(self.scrollView)
        self.scrollView.addSubview(self.stackView)
        self.scrollView.alwaysBounceVertical = true
        self.scrollView.keyboardDismissMode = .onDrag
        
        // 스택 뷰 레이아웃
        self.stackView.spacing = 24
        self.stackView.axis = .vertical
        
        // 스택 뷰, 스크롤 뷰 AutoLayout을 사용하기 위함
        self.scrollView.translatesAutoresizingMaskIntoConstraints = false
        self.stackView.translatesAutoresizingMaskIntoConstraints = false
                
        // AutoLayout
        NSLayoutConstraint.activate([
            self.scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            self.scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            self.scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            self.scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            self.scrollView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor),
            
            self.stackView.topAnchor.constraint(equalTo: self.scrollView.topAnchor),
            self.stackView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor, constant: 24),
            self.stackView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor, constant: -24),
            self.stackView.widthAnchor.constraint(equalTo: self.scrollView.widthAnchor, constant: -48),
            self.stackView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor)
        ])
        
        // 버튼 추가 및 설정
        self.view.addSubview(self.button)
        self.button.backgroundColor = .systemBlue
        self.button.setTitle("결제하기", for: .normal)
        self.button.addTarget(self, action: #selector(self.requestPayment), for: .touchUpInside)

        // 결제 위젯을 랜더링하는 메서드
        let paymentMethods = self.widget.renderPaymentMethods(amount: PaymentMethodWidget.Amount(value: 10000))
        // 이용약관을 랜더링하는 메서드
        let agreement = self.widget.renderAgreement()

        // 랜더링 결과들을 스택 뷰에 Add
        self.stackView.addArrangedSubview(paymentMethods)
        self.stackView.addArrangedSubview(agreement)
        self.stackView.addArrangedSubview(self.button)

        // 위젯 결과를 위한 Delegate
        self.widget.delegate = self
        self.widget.paymentMethodWidget?.widgetStatusDelegate = self
    }
    
    // 고객이 선택한 결제 수단의 결제창을 띄우는 메서드
    @objc func requestPayment() {
        self.widget.requestPayment(
            info: DefaultWidgetPaymentInfo(
            orderId: "2VAhXURbYbiKwX5ybfrLr",
            orderName: "토스 티셔츠 외 2건")
        )
    }
}



extension ViewController: TossPaymentsDelegate {
    // 결제가 성공한 경우
    public func handleSuccessResult(_ success: TossPaymentsResult.Success) {
        print("결제 성공")
        print("paymentKey: \(success.paymentKey)")
        print("orderId: \(success.orderId)")
        print("amount: \(success.amount)")
    }

    // 결제가 실패한 경우
    public func handleFailResult(_ fail: TossPaymentsResult.Fail) {
        print("결제 실패")
        print("errorCode: \(fail.errorCode)")
        print("errorMessage: \(fail.errorMessage)")
        print("orderId: \(fail.orderId)")
    }
}

extension ViewController: TossPaymentsWidgetStatusDelegate {
    // 결제위젯 렌더링이 완료되면 호출되는 메서드
    public func didReceivedLoad(_ name: String) {
        print("결제위젯 렌더링 완료 ")
    }
}

SDK가 설치되어 있다면 붙여 넣기 만으로도 실행하는데 문제없을 것입니다. 

 

 

 

 

 

토스페이먼츠 (tosspayments.com)

 

토스페이먼츠

온라인 결제의 새로운 기준

www.tosspayments.com

토스페이먼츠 개발자센터 (tosspayments.com)

 

토스페이먼츠 개발자센터

원하는 언어로 바로 시작해보세요 언어별 코드 예제를 복사, 붙여넣기만 하면 연동 끝!

developers.tosspayments.com

tosspayments/payment-sdk-ios: Toss Payments iOS SDK (github.com)

 

GitHub - tosspayments/payment-sdk-ios: Toss Payments iOS SDK

Toss Payments iOS SDK. Contribute to tosspayments/payment-sdk-ios development by creating an account on GitHub.

github.com

SwiftUI vs UIKit, 뭘 사용해야 돼요? (velog.io)

 

SwiftUI vs UIKit, 뭘 사용해야 돼요?

UIKit, SwiftUI 프레임워크의 차이점을 알아보고 iOS 앱에 토스페이먼츠 결제위젯을 연동해볼게요.

velog.io

 

반응형