모바일 결제 SDK: Apple Pay와 Google Pay 통합 구현

모바일 개발

Apple PayGoogle Pay모바일 결제StripePassKit

이 글은 누구를 위한 것인가

  • 앱 내 결제를 Apple Pay/Google Pay로 간소화하려는 팀
  • 카드 입력 없이 원탭 결제를 구현하고 싶은 개발자
  • Stripe와 Apple Pay를 연동하는 방법이 필요한 엔지니어

들어가며

Apple Pay는 결제 전환율을 30-40% 높인다. 카드 번호 입력이 없고 Face ID/Touch ID로 즉시 결제되기 때문이다. 구현은 생각보다 간단하다.

이 글은 bluebutton.kr의 모바일 결제 통합 가이드 를 참고하여 작성했습니다.


1. Apple Pay / Google Pay 아키텍처

[Apple Pay 처리 흐름]

1. 앱: PKPaymentRequest 생성
2. iOS: 결제 시트 표시 (Face ID 인증)
3. Apple: 결제 토큰 생성 (암호화)
4. 앱: 토큰을 서버로 전송
5. 서버: 결제 프로세서(Stripe)에 토큰 전달
6. Stripe: Apple에 복호화 요청 → 결제 처리
7. 서버: 결제 결과를 앱에 반환

[보안 특징]
  카드 번호가 앱/서버에 전달되지 않음
  각 거래마다 다른 DAN (Device Account Number)
  PCI DSS 범위 최소화

[Google Pay 처리 흐름]
  동일한 패턴 (Android Pay가 Google Pay로 통합)
  PaymentsClient → 결제 데이터 → 서버

[결제 프로세서 선택]
  Stripe: 국제 표준, 한국 원화 지원 ✓
  NHN KCP, 이니시스: 국내 특화
  Braintree: PayPal 계열

[Merchant ID 설정]
  Apple Developer Console에서 생성
  merchant.com.company.appname 형식
  결제 처리 서버 인증서 설정 필요

2. Apple Pay 구현

import PassKit
import StripePaymentSheet

class ApplePayManager: NSObject {
    
    // Apple Pay 사용 가능 여부 확인
    static func isAvailable() -> Bool {
        PKPaymentAuthorizationViewController.canMakePayments(
            usingNetworks: [.visa, .masterCard, .amex]
        )
    }
    
    // 결제 요청 생성
    func createPaymentRequest(
        amount: NSDecimalNumber,
        currency: String = "KRW",
        items: [(label: String, amount: NSDecimalNumber)],
    ) -> PKPaymentRequest {
        let request = PKPaymentRequest()
        request.merchantIdentifier = "merchant.com.myapp.payments"
        request.countryCode = "KR"
        request.currencyCode = currency
        request.supportedNetworks = [.visa, .masterCard, .amex]
        request.merchantCapabilities = [.capability3DS, .capabilityCredit, .capabilityDebit]
        
        // 결제 항목
        request.paymentSummaryItems = items.map {
            PKPaymentSummaryItem(label: $0.label, amount: $0.amount)
        } + [PKPaymentSummaryItem(label: "총 결제금액", amount: amount)]
        
        // 배송지 요청 (필요한 경우)
        request.requiredShippingContactFields = [.postalAddress, .name]
        
        return request
    }
    
    // 결제 시트 표시
    func presentPaymentSheet(
        from viewController: UIViewController,
        request: PKPaymentRequest,
        completion: @escaping (PKPaymentAuthorizationResult) -> Void,
    ) {
        guard let authVC = PKPaymentAuthorizationViewController(paymentRequest: request) else {
            return
        }
        authVC.delegate = self
        self.completionHandler = completion
        viewController.present(authVC, animated: true)
    }
    
    private var completionHandler: ((PKPaymentAuthorizationResult) -> Void)?
    private var paymentSucceeded = false
}

extension ApplePayManager: PKPaymentAuthorizationViewControllerDelegate {
    
    func paymentAuthorizationViewController(
        _ controller: PKPaymentAuthorizationViewController,
        didAuthorizePayment payment: PKPayment,
        handler completion: @escaping (PKPaymentAuthorizationResult) -> Void,
    ) {
        // 결제 토큰을 서버로 전송
        Task {
            do {
                let token = payment.token
                let tokenData = token.paymentData
                
                // Stripe: paymentData를 서버에 전달
                try await processPaymentWithServer(
                    paymentData: tokenData,
                    billingContact: payment.billingContact,
                )
                
                paymentSucceeded = true
                completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
            } catch {
                completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
            }
        }
    }
    
    func paymentAuthorizationViewControllerDidFinish(
        _ controller: PKPaymentAuthorizationViewController,
    ) {
        controller.dismiss(animated: true) {
            if self.paymentSucceeded {
                self.completionHandler?(PKPaymentAuthorizationResult(status: .success, errors: nil))
            }
        }
    }
    
    private func processPaymentWithServer(
        paymentData: Data,
        billingContact: PKContact?,
    ) async throws {
        let base64Token = paymentData.base64EncodedString()
        
        let body: [String: Any] = [
            "apple_pay_token": base64Token,
            "billing_name": billingContact?.name?.formatted() ?? "",
        ]
        
        var request = URLRequest(url: URL(string: "https://api.myapp.com/payments/apple-pay")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        
        let (_, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw PaymentError.serverError
        }
    }
}

enum PaymentError: Error {
    case serverError
    case tokenGenerationFailed
}

// Stripe와 Apple Pay 통합 (더 간단한 방법)
class StripeApplePayManager {
    
    func presentStripePaymentSheet(
        from viewController: UIViewController,
        amount: Int,  // 센트 단위
        currency: String = "krw",
    ) async throws {
        // 1. 서버에서 PaymentIntent 생성
        let clientSecret = try await createPaymentIntent(amount: amount, currency: currency)
        
        // 2. Stripe PaymentSheet 설정
        var configuration = PaymentSheet.Configuration()
        configuration.merchantDisplayName = "My App"
        configuration.applePay = .init(
            merchantId: "merchant.com.myapp.payments",
            merchantCountryCode: "KR",
        )
        
        let paymentSheet = PaymentSheet(
            paymentIntentClientSecret: clientSecret,
            configuration: configuration,
        )
        
        // 3. 결제 시트 표시
        return try await withCheckedThrowingContinuation { continuation in
            paymentSheet.present(from: viewController) { result in
                switch result {
                case .completed:
                    continuation.resume()
                case .failed(let error):
                    continuation.resume(throwing: error)
                case .canceled:
                    continuation.resume(throwing: PaymentError.serverError)
                }
            }
        }
    }
    
    private func createPaymentIntent(amount: Int, currency: String) async throws -> String {
        // 서버 API 호출
        return "pi_xxx_secret_yyy"  // 실제 구현
    }
}

마무리

Apple Pay 구현의 핵심은 결제 토큰이 앱 서버를 통해서만 처리되게 하는 것이다. 앱 코드에 카드 정보나 결제 프로세서 API 키를 절대 넣지 말라. Stripe를 쓰면 PaymentSheet로 Apple Pay, Google Pay, 카드 결제를 하나의 UI로 통합할 수 있어 구현이 훨씬 단순해진다.