이 글은 누구를 위한 것인가
- 앱 내 결제를 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로 통합할 수 있어 구현이 훨씬 단순해진다.