Jiwift

[iOS/Swift] Toss Payments 자동 결제(빌링) 연동하기 본문

라이브러리/TossPayments

[iOS/Swift] Toss Payments 자동 결제(빌링) 연동하기

지위프트 2023. 11. 2. 00:03
반응형

[iOS/Swift] Toss Payments - 자동 결제(빌링) 연동하기 (정기 구독, 구독제)

 

 

 지난 시간에는 정기 결제를 이해하는 글을 작성했습니다. 이번에는 연동하는 방법을 알아보도록 하겠습니다. 우선 이 글은 SDK가 아닌 웹뷰 방식으로 연동하는 점을 참고하고 글을 읽어주세요. 

 

코드가 이해가 안 가시는 분은 다 읽고 맨 아래 전체 코드를 참고해 주세요.

Xcode Storyboard

 웹뷰 방식을 연동하기 위해서는 WebKit View를 사용해야 합니다. Storyboard 혹은 코드를 통해서 선언해 주세요. 저는 Storyboard를 통해서 하겠습니다. 

 

Xcode

 Storyboard로 진행하시는 분들은 위와 같이 연결과 webkit을 import 해주세요. 다음은 웹뷰를 띄어야 합니다. 

 

<head>
  <title>결제하기</title>
  <meta charset="utf-8" />
  <!-- 토스페이먼츠 결제창 SDK 추가 -->
  <script src="https://js.tosspayments.com/v1/payment"></script>
</head>
<body>
  <script>
    // ------ 클라이언트 키로 객체 초기화 ------
    var clientKey = 'test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq'
    var tossPayments = TossPayments(clientKey)
    tossPayments.requestBillingAuth('CARD', { // 결제수단 파라미터 (자동결제는 카드만 지원합니다.)
      // 결제 정보 파라미터
      customerKey: 'Vj_mNuQtPv_f9hNcDMb4L', // 고객 ID로 상점에서 만들어야 합니다. 빌링키와 매핑됩니다. 자세한 파라미터 설명은 파라미터 설명을 참고하세요: https://docs.tosspayments.com/reference/js-sdk#결제-정보-5
      successUrl: "https://my-store.com/success", // 카드 등록에 성공하면 이동하는 페이지(직접 만들어주세요)
      failUrl: "https://my-store.com/fail",       // 카드 등록에 실패하면 이동하는 페이지(직접 만들어주세요)
    })
    .catch(function (error) {
      if (error.code === 'USER_CANCEL') {
        // 결제 고객이 결제창을 닫았을 때 에러 처리
      }
    })
  </script>
</body>

 저 같은 경우는 따로 웹을 구축하지 않고 위 HTML을 바로 웹뷰에 넣을 생각입니다. 

 

// 토스 예제 클라이언트 키
let clientKey: String = "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq"
// 토스 예제 고객 키
let customerKey: String = "Vj_mNuQtPv_f9hNcDMb4L"

// 성공/실패시 이동할 URL
let cardSuccessUrl: String = "http://localhost:8080/card/success"
let cardFailUrl: String =  "http://localhost:8080/card/fail"

let tossWeb: String = """
                    <head>
                      <title>결제하기</title>
                      <meta charset="utf-8" />
                      <!-- 토스페이먼츠 결제창 SDK 추가 -->
                      <script src="https://js.tosspayments.com/v1/payment"></script>
                    </head>
                    <body>
                      <script>
                        // ------ 클라이언트 키로 객체 초기화 ------
                        var clientKey = '\(clientKey)'
                        var tossPayments = TossPayments(clientKey)
						// 결제수단 파라미터 (자동결제는 카드만 지원합니다.)
                        tossPayments.requestBillingAuth('CARD', { 
                          // 결제 정보 파라미터
                          customerKey: '\(customerKey)', // 고객 ID로 상점에서 만들어야 합니다. 빌링키와 매핑됩니다. 자세한 파라미터 설명은 파라미터 설명을 참고하세요: https://docs.tosspayments.com/reference/js-sdk#결제-정보-5
                          successUrl: "\(cardSuccessUrl)", // 카드 등록에 성공하면 이동하는 페이지(직접 만들어주세요)
                          failUrl: "\(cardFailUrl)",       // 카드 등록에 실패하면 이동하는 페이지(직접 만들어주세요)
                        })
                        .catch(function (error) {
                          if (error.code === 'USER_CANCEL') {
                            // 결제 고객이 결제창을 닫았을 때 에러 처리
                          }
                        })
                      </script>
                    </body>
                    """

 Swift로 작성한 예시입니다. 서버에서 받게 될 정보를 HTML 중간에 넣어주는 방식으로 변수를 생성했습니다. 그리고 cardSuccessUrl, cardFailUrl을 선언했는데요. 저건 그냥 토스에서 최종 결과를 어느 곳으로 이동해서 보여줄지? 에 대한 URL입니다. 진짜 작동하지 않더라도 저런 식으로 적어두기만 해도 앱에서는 파싱용으로 사용하기 때문에 문제가 되지 않습니다.

 

변수 선언 위치나 방법은 사람마다 다를 수 있다고 생각됩니다.

 

다음은 Webkit입니다. 

self.tossBillingWKView.navigationDelegate = self
self.tossBillingWKView.loadHTMLString(self.tossWeb, baseURL: nil)

 화면을 띄우는 방법은 간단합니다. 생성한 웹킷 뷰에 'loadHTMLString'을 통해서 HTML을 코드를 넣어주면 됩니다. 예제를 통해서 실행하는 것이기 때문에 화면이 나오고 결제는 진행되지 않습니다. 'navigation Delegate'는 결과를 파싱 하기 위해서 사용됩니다. 

 

 

실행된 화면

 위 상태로 코드를 실행하면 토스 화면이 나오게 됩니다. 

 

(작성하면서 테스트 여러 번 해본 결과 토스 Test Key는 카드 정보 입력 다음 화면으로 진행되지 않는 것 같습니다. 원래는 되었던 것 같은데...)

 

화면이 나왔으니 고객의 카드와 본인인증 결과를 앱에서 확인을 해야 합니다. 파싱을 위한 코드를 작성할 건데요. 앞서 채택한 'navigation Delegate'를 활용합니다.

 

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    guard let url = navigationAction.request.url else {
        decisionHandler(.allow)
        return
    }

    let urlString = url.absoluteString
    print("결과. :\(urlString)")

    // Check if the URL contains "/success"
    if urlString.contains("/card/success") {
        // 성공인 경우

        let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
        // Customer Key
        let customerKey = urlComponents?.queryItems?.first(where: { $0.name == "customerKey" })?.value
        // Auth Key
        let authKey = urlComponents?.queryItems?.first(where: { $0.name == "authKey" })?.value

        decisionHandler(.cancel)
    } else if urlString.contains("/card/fail") {
        // 실패한 경우

        let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
        // Error Code
        let code = urlComponents?.queryItems?.first(where: { $0.name == "code" })?.value
        // Error Message
        let message = urlComponents?.queryItems?.first(where: { $0.name == "message" })?.value

        decisionHandler(.cancel)
    } else {
        decisionHandler(.allow)
    }
}

 토스는 결과를 우리가 지정한 URL(위에서 넣어준 거)로 이동하게 됩니다. 실제로 동작하지 않는 곳이라도 WebKit은 그 URL을 읽고 파싱 해서 데이터를 뽑아낼 수 있습니다. 

 

 글에서는 데이터를 뽑아내는 것까지 진행했지만 실제로는 다음에 서버와 작업을 진행하는 것을 구현했습니다. 후에 작업은 같이 개발하는 서버 개발자분들과 함께 대화를 통해서 어떻게 API를 전송할지 고민하면 될 것 같습니다.

 

처음이 어렵지 한번 이해하면 생각보다 별거 아닌 결제 연동. 

 

//
//  BillingViewController.swift
//  TossPaymentWidget
//
//  Created by 김지태 on 10/31/23.
//

import UIKit
import WebKit

class BillingViewController: UIViewController {

    @IBOutlet weak var tossBillingWKView: WKWebView!
   
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 토스 예제 클라이언트 키
        let clientKey: String = "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq"
        // 토스 예제 고객 키
        let customerKey: String = "Vj_mNuQtPv_f9hNcDMb4L"
        
        // 성공/실패시 이동할 URL
        let cardSuccessUrl: String = "http://localhost:8080/card/success"
        let cardFailUrl: String =  "http://localhost:8080/card/fail"
        
        let tossWeb: String = """
                            <head>
                              <title>결제하기</title>
                              <meta charset="utf-8" />
                              <!-- 토스페이먼츠 결제창 SDK 추가 -->
                              <script src="https://js.tosspayments.com/v1/payment"></script>
                            </head>
                            <body>
                              <script>
                                // ------ 클라이언트 키로 객체 초기화 ------
                                var clientKey = '\(clientKey)'
                                var tossPayments = TossPayments(clientKey)
                            
                                tossPayments.requestBillingAuth('CARD', { // 결제수단 파라미터 (자동결제는 카드만 지원합니다.)
                                  // 결제 정보 파라미터
                                  customerKey: '\(customerKey)', // 고객 ID로 상점에서 만들어야 합니다. 빌링키와 매핑됩니다. 자세한 파라미터 설명은 파라미터 설명을 참고하세요: https://docs.tosspayments.com/reference/js-sdk#결제-정보-5
                                  successUrl: "\(cardSuccessUrl)", // 카드 등록에 성공하면 이동하는 페이지(직접 만들어주세요)
                                  failUrl: "\(cardFailUrl)",       // 카드 등록에 실패하면 이동하는 페이지(직접 만들어주세요)
                                })
                                .catch(function (error) {
                                  if (error.code === 'USER_CANCEL') {
                                    // 결제 고객이 결제창을 닫았을 때 에러 처리
                                  }
                                })
                              </script>
                            </body>
                            """
        // 토스 웹킷 Delegate
        self.tossBillingWKView.navigationDelegate = self
        
        self.tossBillingWKView.loadHTMLString(tossWeb, baseURL: nil)
        
        
    }
}

extension BillingViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }
        
        let urlString = url.absoluteString
        print("결과. :\(urlString)")
        
        // Check if the URL contains "/success"
        if urlString.contains("/card/success") {
            // 성공인 경우
            
            let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
            // Customer Key
            let customerKey = urlComponents?.queryItems?.first(where: { $0.name == "customerKey" })?.value
            // Auth Key
            let authKey = urlComponents?.queryItems?.first(where: { $0.name == "authKey" })?.value
            
            decisionHandler(.cancel)
        } else if urlString.contains("/card/fail") {
            // 실패한 경우
            
            let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
            // Error Code
            let code = urlComponents?.queryItems?.first(where: { $0.name == "code" })?.value
            // Error Message
            let message = urlComponents?.queryItems?.first(where: { $0.name == "message" })?.value
            
            decisionHandler(.cancel)
        } else {
            decisionHandler(.allow)
        }
    }
}
반응형