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

by 토스페이먼츠

[모바일 앱 결제의 모든 것] SwiftUI vs UIKit, 뭘 사용해야 돼요?

고객의 결제 경험은 매출과 연결되기 때문에 앱의 매우 중요한 부분이죠. 결제를 웹뷰로 연동할 수도 있지만, 웹뷰는 속도가 비교적 느리고 UI가 제한적이에요. 모바일 앱에서 결제를 빠르고 간편하게 연동하고 싶다면 토스페이먼츠 Native SDK를 사용해보세요. 지난번에는 Android에서 결제위젯을 연동해봤는데요. 오늘은 UIKit, SwiftUI 프레임워크의 차이점을 알아보고 iOS 앱애 결제위젯을 연동해볼게요.

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

Swift 개발자라면 UIKit, SwiftUI라는 용어를 들어봤을 텐데요. 둘 다 iOS 앱의 UI를 만드는 프레임워크이지만, 중요한 차이점을 가지고 있어요.

SwiftUI는 2019년 iOS 13과 함께 출시된 새로운 iOS 개발 프레임워크에요. 가장 큰 특징은 ‘선언형’이라는 점인데요. 선언형 프로그래밍에서는 UI에 표시될 데이터만 정의하면, 데이터를 ‘어떻게’ 표시할지는 프레임워크가 결정해요. 개발 과정이 단순하고, 복잡한 UI도 적은 양의 코드로 만들 수 있다는 장점을 가지고 있어요. 간단한 앱을 빠르게 개발하고 싶다면 SwiftUI를 추천해요.

반면 UIKit는 첫 iOS 릴리즈부터 사용된 ‘명령형’ 프레임워크에요. 명령형 프레임워크에서는 View를 직접 생성하고 제어할 수 있어요. 그래서 UI를 더 섬세하게 조절할 수 있고 SwiftUI보다 더 많은 기능과 UI를 제공해요. 커스터마이징이 많이 필요한 UI를 기획하고 있다면 UIKit를 추천해요.

필요에 따라 UIKit와 SwiftUI를 한 앱에서 같이 사용할 수도 있어요. UIKit 앱의 일부를 SwiftUI로 구현하거나 두 프레임워크 간에 인터페이스를 혼합하면 돼요. 예를 들어, SwiftUI를 UIKit의 UIViewController에서 사용할 수 있고, UIView를 SwiftUI의 View 안에서 사용할 수 있어요.

iOS 결제 주문서 만들기

오늘은 UIKit 프레임워크에서 토스페이먼츠 결제위젯으로 주문서 페이지를 만들어볼게요.

프로젝트 설정하기

  1. XCode에서 새로운 iOS App 프로젝트를 생성하세요. UIKit를 사용하기 위해 프로젝트 옵션에 Interface를 Storyboard로 선택해주세요.
  2. Cocoapods 또는 Swift Package Manager(SPM)으로 토스페이먼츠 SDK를 설치하세요.

ViewController 만들기

UIKit에서 UIViewController는 말 그대로 View를 관리하는 Controller인데요. 화면의 레이아웃 구성을 제어하고 사용자가 화면과 어떻게 상호작용하는지 정의해요. 이번 프로젝트에서는 간단하게 UIScrollView 안에 UIStackView를 넣어서 주문서 페이지를 만들어볼게요.

//ViewController.swift

import UIKit

open class ViewController: UIViewController {
    public lazy var scrollView = UIScrollView()
    public lazy var stackView = UIStackView()
    
    public var scrollViewBottomAnchorConstraint: NSLayoutConstraint?
    open override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        view.addSubview(scrollView)
        scrollView.addSubview(stackView)
        scrollView.alwaysBounceVertical = true
        scrollView.keyboardDismissMode = .onDrag
        
        stackView.spacing = 24
        stackView.axis = .vertical
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            scrollView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor),
            
            stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 24),
            stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -24),
            stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -48),
            stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
        ])
    }
}

결제위젯 연동하기

이제 본격적으로 주문서 View를 만들어볼게요. 프로젝트에 PaymentWidgetViewController.swift 파일을 새로 만들어주세요. View를 로드하기 전에 결제위젯 인스턴스를 생성할게요. 결제위젯 생성자에 클라이언트 키, 고객 키를 넣어주세요.

  • clientKey: 토스페이먼츠에서 발급하는 연동 키입니다. API 키 페이지에서 테스트 클라이언트 키값을 사용하세요. 아래 코드에 있는 테스트용 클라이언트 키를 사용할 수도 있어요.
  • customerKey: 고객 ID입니다. 다른 사용자가 이 값을 알게 되면 악의적으로 사용할 수 있어 자동 증가하는 숫자는 안전하지 않습니다. UUID와 같이 충분히 무작위적인 고유 값으로 생성해주세요. 영문 대소문자, 숫자, 특수문자 -, _=.@ 를 최소 1개 이상 포함한 최소 2자 이상 최대 300자 이하의 문자열이어야 합니다.
import UIKit
import TossPayments

class PaymentWidgetViewController: ViewController {
	private lazy var widget: PaymentWidget = PaymentWidget(
	    clientKey: "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq",
	    customerKey: "EPUx4U0_zvKaGMZkA7uF_"
	)
	override func viewDidLoad() {
	    super.viewDidLoad()
	}
}

View를 로드한 뒤에 결제위젯 인스턴스로 결제위젯과 이용약관 위젯을 렌더링하세요. View에 렌더링을 추가하면 이제 아래 화면과 같이 결제위젯과 이용약관위젯이 보일 거예요.

override func viewDidLoad() {
    super.viewDidLoad()
		let paymentMethods = widget.renderPaymentMethods(amount: PaymentMethodWidget.Amount(value: 10000))
		let agreement = widget.renderAgreement()
		
		stackView.addArrangedSubview(paymentMethods)
		stackView.addArrangedSubview(agreement)
}

하지만 중요한 ‘결제하기’ 버튼을 아직 안 만들었어요. 버튼을 View에 추가해주고, ‘결제하기’ 문구를 버튼에 넣어주세요. 사용자가 ‘결제하기’ 버튼을 누르면 결제를 요청하는 requestPayment() 메서드를 호출할게요. 이 메서드에는 DefaultWidgetPaymentInfo를 사용해서 결제 정보를 추가할 수 있어요. DefaultWidgetPaymentInfo는 확장가능한 Protocol이에요. 주문 ID와 주문명은 필수 파라미터에요. 더 많은 파라미터는 SDK 레퍼런스 문서에서 확인하세요.

  • orderId: 주문을 구분하는 ID입니다. 충분히 무작위한 값을 생성해서 각 주문마다 고유한 값을 넣어주세요. 영문 대소문자, 숫자, 특수문자 -_=로 이루어진 6자 이상 64자 이하의 문자열이어야 합니다. 유니크한 값이어야 합니다.
  • orderName: 주문명입니다. 예를 들면 생수 외 1건 같은 형식입니다. 최대 길이는 100자입니다.
 private lazy var button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(button)
        button.setTitle("결제하기", for: .normal)
        button.addTarget(self, action: #selector(requestPayment), for: .touchUpInside)

        let paymentMethods = widget.renderPaymentMethods(amount: PaymentMethodWidget.Amount(value: 10000))
        let agreement = widget.renderAgreement()
        
        stackView.addArrangedSubview(paymentMethods)
        stackView.addArrangedSubview(agreement)
        stackView.addArrangedSubview(button)
		}

    @objc func requestPayment() {
        widget.requestPayment(
            info: DefaultWidgetPaymentInfo(
                orderId: "2VAhXURbYbiKwX5ybfrLr",
                orderName: "토스 티셔츠 외 2건"),
            on: self
        )
		}

결제 요청 결과는 결제위젯 delegate으로 받을 수 있어요. 결제 요청이 성공하면 handleSuccessResult()가 호출되고 결제 요청이 실패하면 handleFailResult()이 호출돼요. 아래 코드에서는 결제 요청 성공, 실패 시 간단한 로그를 남기고 있는데요. 실제로 결제를 완성하고 싶다면 handleSuccessResult()안에서 결제 승인을 요청해주세요.

class PaymentWidgetViewController: ViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
				//...
        widget.delegate = self
    }
}

extension PaymentWidgetViewController: 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)")

    }
}

기본적으로 결제위젯을 연동하고 결제를 요청하는 과정은 마무리됐어요. 기본 결제 외에 결제위젯은 더 많은 기능을 제공하고 있는데요. 예를 들어, 결제위젯 렌더링이 완료됐는지 알고 싶다면 아래와 같이 TossPaymentsWidgetStatusDelegate으로 해당 정보를 받을 수 있어요. 결제위젯과 동시에 보여주고 싶은 View나 컴포넌트는 didReceivedLoad()안에서 로드를 해주세요.

class PaymentWidgetViewController: ViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
				//...
        widget.paymentMethodWidget?.widgetStatusDelegate = self;
    }
}

extension PaymentWidgetViewController: TossPaymentsWidgetStatusDelegate {
    public func didReceivedLoad(_ name: String) {
        print("결제위젯 렌더링 완료 ")
    }
}

다음 단계는?

결제위젯을 화면에 띄우고 결제 요청까지 완료했는데요. handleSuccessResult()에 돌아온 정보로 결제 승인에 성공해야 결제가 완료됩니다. 결제 화면에서 각 은행・카드사 앱으로 잘 이동하는지도 확인을 해야 되고요. 만약에 금액이 쿠폰 등 이유로 바뀐다면 위젯 UI에 금액도 업데이트해줘야 되죠. 자세한 내용은 결제위젯 연동 가이드결제위젯 iOS SDK 레퍼런스를 참고하세요.

전체 코드 복사하기

//PaymentWidgetViewController.swift

import UIKit
import TossPayments

class PaymentWidgetViewController: ViewController {
    
    private lazy var widget: PaymentWidget = PaymentWidget(
        clientKey: "test_ck_D5GePWvyJnrK0W0k6q8gLzN97Eoq",
        customerKey: "EPUx4U0_zvKaGMZkA7uF_"
    )

    private lazy var button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(button)
        button.backgroundColor = .systemBlue
        button.setTitle("결제하기", for: .normal)
        button.addTarget(self, action: #selector(requestPayment), for: .touchUpInside)

        let paymentMethods = widget.renderPaymentMethods(amount: PaymentMethodWidget.Amount(value: 10000))
        let agreement = widget.renderAgreement()
        
        stackView.addArrangedSubview(paymentMethods)
        stackView.addArrangedSubview(agreement)
        stackView.addArrangedSubview(button)
        
        widget.delegate = self
        widget.paymentMethodWidget?.widgetStatusDelegate = self;
    }

    @objc func requestPayment() {
        widget.requestPayment(
            info: DefaultWidgetPaymentInfo(
                orderId: "2VAhXURbYbiKwX5ybfrLr",
                orderName: "토스 티셔츠 외 2건"),
            on: self
        )
    }
    
}

extension PaymentWidgetViewController: 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 PaymentWidgetViewController: TossPaymentsWidgetStatusDelegate {
    public func didReceivedLoad(_ name: String) {
        print("결제위젯 렌더링 완료 ")
    }
}

함께 읽으면 좋을 콘텐츠

📍 인앱 결제 vs. PG 결제, 뭘 사용해야 돼요?

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

Writer 박수연 Graphic 이은호, 이나눔

    의견 남기기
    토스페이먼츠

    고객사의 성장이 곧 우리의 성장이라는 확신을 가지고 더 나은 결제 경험을 만듭니다. 결제가 불편한 순간을 기록하고 바꿔갈게요.