개요
0:01:30무료 체험은 구독 앱에서 가장 효과적인 전환 도구 가운데 하나입니다. Apple은 이를 도입 오퍼(introductory offer)라고 부르며, 다음 세 가지 유형이 있습니다.
- 무료 체험: 사용자가 정해진 기간 동안 무료로 이용한 뒤, 정가로 청구됩니다.
- Pay as you go: 사용자가 정해진 청구 주기 동안 할인된 가격을 냅니다.
- Pay up front: 사용자가 정해진 기간에 해당하는 할인 금액을 한 번에 낸 뒤, 정가로 전환됩니다.
이 가이드는 가장 많이 쓰이고 전환 효과가 가장 큰 무료 체험 유형을 다룹니다. 다른 유형을 감지하고
표시하는 코드 패턴도 동일하며, paymentMode 값만 다릅니다.
1단계: App Store Connect에서 설정하기
0:02:30RevenueCat이 체험을 표시하려면 먼저 App Store Connect에서 무료 체험을 설정해야 합니다.
- App Store Connect에 로그인해 대상 앱을 엽니다.
- 사이드바에서 Subscriptions로 이동합니다.
- 구독 그룹을 선택한 뒤, 대상 구독 상품을 클릭합니다.
- Introductory Offers까지 스크롤한 뒤 + 버튼을 클릭합니다.
- Start Date와 End Date를 설정합니다(무기한으로 운영하려면 End Date를 비워 둡니다).
- 무료 체험을 만들려면 Price를 Free로 설정합니다.
-
Duration을 선택합니다.
- 3일, 1주, 2주, 1개월, 2개월, 3개월, 6개월, 1년
- Eligibility를 New Subscribers Only로 설정합니다(표준적인 선택입니다).
- Save를 클릭합니다.
2단계: 체험 대상 여부 확인하기
0:02:30
Offering을 가져온 뒤, 각 StoreProduct의 introductoryDiscount 프로퍼티를 살펴보세요.
이 값이 존재하고 그 paymentMode가 .freeTrial이면, 그 사용자는 해당 상품의
체험 대상입니다.
// Fetch offerings and inspect trial eligibility per package
func fetchOfferingsAndCheckTrials() async {
do {
let offerings = try await Purchases.shared.offerings()
guard let current = offerings.current else { return }
for package in current.availablePackages {
let product = package.storeProduct
if let intro = product.introductoryDiscount {
switch intro.paymentMode {
case .freeTrial:
// e.g. "7-day free trial"
print("\(product.productIdentifier): Free trial for \(intro.subscriptionPeriod)")
case .payAsYouGo:
// Discounted price for N periods
print("\(product.productIdentifier): \(intro.localizedPriceString) for \(intro.numberOfPeriods) periods")
case .payUpFront:
// One upfront reduced payment
print("\(product.productIdentifier): \(intro.localizedPriceString) upfront")
default:
break
}
} else {
print("\(product.productIdentifier): No introductory offer available")
}
}
} catch {
print("Failed to fetch offerings: \(error)")
}
}
특정 사용자가 대상인지(이미 체험을 사용했는지까지 반영해) 확인하려면, RevenueCat 전용 대상 확인 API를 사용하세요.
// Check if a specific user is eligible for the intro offer
let productIds = ["com.yourapp.premium_monthly"]
let eligibility = await Purchases.shared.checkTrialOrIntroDiscountEligibility(productIdentifiers: productIds)
if eligibility["com.yourapp.premium_monthly"]?.status == .eligible {
// Show "Start your 7-day free trial" messaging
} else {
// Show regular pricing — user has already used the trial
}
3단계: 페이월 UI에 체험 표시하기
0:03:00
패키지의 introductoryDiscount를 읽어 체험 안내를 조건부로 표시하는 가격 카드 컴포넌트를
만듭니다. 이 패턴은 어떤 RevenueCat Offering 구성에서도 동작하며, App Store Connect에서 설정한 체험
기간에 맞춰 자동으로 조정됩니다.
import SwiftUI
import RevenueCat
struct PackagePricingCard: View {
let package: Package
let onPurchase: (Package) -> Void
private var product: StoreProduct { package.storeProduct }
private var freeTrial: StoreProductDiscount? {
guard let intro = product.introductoryDiscount,
intro.paymentMode == .freeTrial else { return nil }
return intro
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Trial badge
if let trial = freeTrial {
HStack(spacing: 4) {
Text("FREE")
.font(.caption2)
.fontWeight(.black)
.foregroundColor(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.green)
.cornerRadius(4)
Text("\(trialPeriodLabel(trial)) free trial")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.green)
}
}
// Pricing
HStack(alignment: .firstTextBaseline, spacing: 2) {
Text(product.localizedPriceString)
.font(.title2)
.fontWeight(.bold)
if let period = product.subscriptionPeriod {
Text("/ \(periodLabel(period))")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
// CTA
Button(action: { onPurchase(package) }) {
Text(freeTrial != nil ? "Start Free Trial" : "Subscribe Now")
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Color.accentColor)
.cornerRadius(12)
}
// Legal disclaimer
if let trial = freeTrial {
Text("Free for \(trialPeriodLabel(trial)), then \(product.localizedPriceString)/\(periodLabel(product.subscriptionPeriod)). Cancel anytime.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(16)
}
private func trialPeriodLabel(_ discount: StoreProductDiscount) -> String {
let period = discount.subscriptionPeriod
switch period.unit {
case .day: return period.value == 7 ? "7-day" : "\(period.value)-day"
case .week: return "\(period.value)-week"
case .month: return "\(period.value)-month"
case .year: return "\(period.value)-year"
default: return "trial"
}
}
private func periodLabel(_ period: SubscriptionPeriod?) -> String {
guard let period = period else { return "period" }
switch period.unit {
case .day: return "day"
case .week: return "week"
case .month: return "month"
case .year: return "year"
default: return "period"
}
}
}
4단계: 구매 후 체험 감지하기
0:02:00
사용자가 "Start Free Trial"을 탭하고 구매가 성공하면, Entitlement의 periodType을 확인해
그 사용자가 활성 체험 중인지 검증합니다. 이를 활용해 "체험 종료까지 X일" 같은 체험 전용 UI를 표시할 수
있습니다.
func purchasePackage(_ package: Package) async {
do {
let result = try await Purchases.shared.purchase(package: package)
let customerInfo = result.customerInfo
if let entitlement = customerInfo.entitlements["premium"] {
if entitlement.periodType == .trial {
// User is in the free trial period
if let trialEnd = entitlement.expirationDate {
let formatter = RelativeDateTimeFormatter()
let timeRemaining = formatter.localizedString(for: trialEnd, relativeTo: Date())
showSuccessMessage("Trial started! Ends \(timeRemaining).")
} else {
showSuccessMessage("Your free trial has started!")
}
} else if entitlement.isActive {
// User purchased directly (no trial)
showSuccessMessage("Welcome to Premium!")
}
}
} catch {
showErrorMessage("Purchase failed: \(error.localizedDescription)")
}
}
체험 만료와 전환
0:02:00
체험이 끝나면 Apple은 사용자를 유료 구독으로 전환하거나(해지하지 않은 경우), Entitlement를 만료시킵니다.
RevenueCat은 이 업데이트를 서버 간 알림(server-to-server notification)으로 받아
customerInfoUpdatedNotification을 발생시키며, 이는 SubscriptionManager가
자동으로 받아 처리합니다.
체험이 끝나기 전에 전환을 적극적으로 유도하려면 willRenew를 확인하세요.
let customerInfo = try await Purchases.shared.getCustomerInfo()
if let premium = customerInfo.entitlements["premium"],
premium.periodType == .trial {
if premium.willRenew {
// Trial will convert — show positive reinforcement
print("Trial converts automatically in \(daysRemaining(until: premium.expirationDate)) days")
} else {
// User cancelled during trial — show win-back prompt
showWinBackOffer()
}
}
func daysRemaining(until date: Date?) -> Int {
guard let date = date else { return 0 }
return Calendar.current.dateComponents([.day], from: Date(), to: date).day ?? 0
}
서버에서 RevenueCat webhook을 구독하면 TRIAL_STARTED,
TRIAL_CONVERTED, TRIAL_CANCELLED 이벤트를 받아 서버 측 후속 처리(이메일,
분석 등)를 실행할 수도 있습니다.
App Store 가이드라인
0:01:30관련 가이드
- iOS 인앱 구매 전체 튜토리얼: RevenueCat iOS 연동을 처음부터 끝까지
- iOS에서 구독 상태 확인하기: CustomerInfo로 기능 접근 제어
- iOS에서 구매 복원하기: 필수 복원 버튼 구현하기
- App Store Connect 설정: App Store Connect에서 구독 설정하기
- 문제 해결: RevenueCat의 흔한 문제와 해결책