💳 구독형 앱 만들기
StoreKit 2의 현대적인 async/await API로 구독 시스템을 구현합니다.
⭐ 난이도: ⭐⭐⭐
⏱️ 예상 시간: 3-4h
📂 App Services
📦 Product 로딩
StoreManager.swift
func loadProducts() async { products = try await Product.products(for: productIDs) }
💰 구매 처리
Purchase.swift
let result = try await product.purchase() switch result { case .success(let verification): guard case .verified(let transaction) = verification else { return } await transaction.finish() case .userCancelled, .pending: break }
🔄 구독 상태 확인
SubscriptionStatus.swift
func checkSubscriptionStatus() async { for await result in Transaction.currentEntitlements { guard case .verified(let transaction) = result else { continue } if transaction.productType == .autoRenewable { // 구독 활성화 isPremium = true } } }
♻️ 구매 복원
Restore.swift
func restorePurchases() async { try? await AppStore.sync() await checkSubscriptionStatus() }
🎨 페이월 UI (HIG)
Apple HIG 페이월 가이드라인: 기능 가치 먼저 보여주기, 명확한 가격, 쉬운 복원 경로
PaywallView.swift
struct PaywallView: View { @State private var products: [Product] = [] @State private var isPurchasing = false var body: some View { VStack(spacing: 20) { // 기능 가치 먼저 보여주기 Text("프리미엄 기능") .font(.largeTitle.bold()) FeatureRow(icon: "star", text: "무제한 사용") FeatureRow(icon: "bolt", text: "광고 제거") FeatureRow(icon: "heart", text: "독점 콘텐츠") Spacer() // 구독 플랜 ForEach(products, id: \.id) { product in Button { purchase(product) } label: { VStack { Text(product.displayName) Text(product.displayPrice) .font(.headline) } .frame(maxWidth: .infinity) .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(12) } } // 복원 버튼 (HIG 필수) Button("구매 복원") { Task { await restorePurchases() } } .font(.caption) } .padding() .task { products = try? await Product.products(for: ["premium_monthly"]) } } func purchase(_ product: Product) { isPurchasing = true Task { let result = try await product.purchase() // ... 구매 처리 isPurchasing = false } } }
🎯 Transaction 리스너
TransactionObserver.swift
func observeTransactions() async { for await result in Transaction.updates { guard case .verified(let transaction) = result else { continue } // 구매 완료 처리 if transaction.revocationDate == nil { // 활성 구독 unlockPremiumFeatures() } else { // 환불됨 lockPremiumFeatures() } await transaction.finish() } }
💡 HIG 체크리스트
✅ 가치 먼저, 가격은 나중에
✅ 명확한 구독 기간과 갱신 주기
✅ 복원 버튼 필수
✅ "구독 관리" 링크 제공
✅ 무료 체험 기간 명시