무엇을 만드나요

이 코드랩에서는 노코드 원격 빌더인 RevenueCat Paywalls V2로 페이월을 디자인하고, RevenueCatUI로 SwiftUI 앱에 네이티브로 표시합니다. 페이월을 코드에 하드코딩하지 않습니다. 대시보드에서 만들고 게시하므로, 나중에 앱 업데이트 없이 여러분이나 마케팅 팀이 직접 페이월을 바꿀 수 있습니다.

페이월은 RevenueCat의 State of Subscription Apps 2026 데이터를 근거로 디자인합니다. 이 리포트는 실제 페이월이 어떻게 만들어지는지 분석했으며, 핵심은 다음과 같습니다.

  • 페이월에 플랜 두 개를 둡니다. 앱의 41~60%가 쓰는 가장 흔한 구성입니다.
  • 연간 옵션에 절약 배지를 붙여 플랜 하나를 강조합니다. 페이월의 74.5%가 가격을 강조합니다.
  • 무료 체험 안내(페이월의 54%)와 "Cancel anytime" 문구로 안심을 줍니다.
  • "Continue" CTA를 씁니다. 가장 흔한 콜투액션이며, 카운트다운 타이머나 진행 바는 넣지 않습니다. 상위 페이월에서는 거의 보이지 않습니다.
사전 조건: SDK가 이미 설정되어 있고 pro entitlement가 있는 RevenueCat 프로젝트가 필요합니다. 처음이신가요? iOS 코드랩이나 SDK 설정 가이드부터 시작하세요.

Paywalls V2란

Paywalls V2는 노코드로 원격 구성이 가능한 완전 네이티브 페이월 빌더입니다. RevenueCat 대시보드에서 시각적으로 디자인하면 SDK가 이를 네이티브로 렌더링합니다(iOS는 SwiftUI, Android는 Jetpack Compose). 원격 방식이라 앱 업데이트를 배포하지 않고도 페이월 전체를 바꿀 수 있습니다.

왜 중요할까요.

  • 배포 없이 반복 개선합니다. 문구, 레이아웃, 가격 강조, 이미지를 대시보드에서 수정합니다.
  • 네이티브입니다. 웹뷰가 아니라 네이티브 컴포넌트로 렌더링되고, 라이트/다크 모드에 맞춰 적응합니다.
  • 실험으로 이어집니다. 페이월이 offering에 연결되므로 A/B 테스트를 할 수 있습니다(10단계).
버전 참고. Paywalls V2는 2025년 6월에 정식 출시(GA)됐습니다. V2 에디터에서 만든 페이월은 purchases-ios 5.16.0 이상이 필요하며, 다른 플랫폼도 최신 SDK 버전이 필요합니다. 이전 빌더는 이제 레거시 페이월이라고 부르며 템플릿 변수 이름이 다릅니다.

Offering과 Package 설정하기

페이월은 offering에 담긴 package를 렌더링하므로, 이것부터 먼저 구성합니다(RevenueCat 대시보드의 프로젝트 아래에서).

  1. 스토어 상품(월간과 연간 구독)이 존재하고 RevenueCat으로 가져왔는지 확인합니다.
  2. 두 상품을 pro entitlement에 연결합니다.
  3. 기본 offeringpackage 두 개, 즉 $rc_monthly$rc_annual package를 추가하고, 각각 맞는 상품을 감쌉니다.
가격은 스토어에서 옵니다. package는 현지화된 가격을 담은 기반 스토어 상품을 감쌉니다. 페이월은 변수를 통해 그 가격을 표시하므로(다음 단계), 앱에서 가격이나 상품 id를 하드코딩할 일이 없습니다.

offering과 package가 처음이신가요? 상품과 가격 가져오기를 참고하세요.

에디터에서 페이월 디자인하기

대시보드에서 Paywalls로 이동한 뒤 Create paywall을 클릭합니다. 템플릿에서 시작하고(권장), 에디터의 페이월 속성("Select an Offering for your Paywall")에서 offering에 연결합니다. 이제 데이터 기반 레이아웃을 만듭니다.

  1. Package 컴포넌트 두 개: 하나는 월간 package, 하나는 연간 package용입니다. 두 플랜 페이월이 가장 흔한 구성입니다.
  2. 연간 플랜을 강조하고 할인 변수로 절약 배지를 붙입니다.
  3. pro로 열리는 기능을 담은 Feature list와 짧은 헤드라인을 넣습니다.
  4. 무료 체험 안내"Cancel anytime" 문구를 넣습니다.
  5. Purchase button(CTA)의 라벨을 "Continue"로 설정합니다.

템플릿 변수(중괄호 두 개)를 쓰면 가격과 혜택이 항상 스토어에서 옵니다.

text
{{ product.price }}              -> "$9.99"          (the package price)
{{ product.price_per_period }}   -> "$59.99/year"    (price with the billing period)
{{ product.relative_discount }}  -> "37%"            (annual savings vs the priciest plan)
{{ product.offer_price }}        -> "free"           (the intro/trial price, when there is one)

만족스러우면 Publish를 클릭합니다. 앱 업데이트 없이 곧바로 적용됩니다.

눈속임 기법은 건너뛰세요. 데이터를 보면 성과가 좋은 상위 페이월에서는 카운트다운 타이머와 진행 바가 거의 보이지 않습니다. Countdown 컴포넌트가 있긴 하지만 쓸 필요가 없습니다. 명확한 플랜, 강조한 연간 옵션, 체험 문구면 충분합니다.

RevenueCatUI 설치하기

페이월은 RevenueCatUI 라이브러리로 렌더링됩니다. Xcode에서 Swift 패키지 https://github.com/RevenueCat/purchases-ios-spm.git을 추가하고 RevenueCatRevenueCatUI 제품을 둘 다 선택하세요.

swift
import SwiftUI
import RevenueCat     // the core SDK
import RevenueCatUI   // the paywall views
최소 버전. RevenueCatUI는 iOS 15.0 이상이 필요하고, Paywalls V2는 purchases-ios 5.16.0 이상이 필요합니다. SDK는 앱 실행 시점에 이미 설정되어 있어야 합니다 (1단계의 사전 조건 참고).

presentPaywallIfNeeded로 접근 제어하기

페이월을 표시하는 가장 간단한 방법은 presentPaywallIfNeeded 뷰 모디파이어입니다. 이 모디파이어는 사용자에게 entitlement가 없을 때만 offering의 페이월을 표시하고, entitlement가 생기면 자동으로 닫힙니다.

swift
import SwiftUI
import RevenueCat
import RevenueCatUI

struct RootView: View {
    var body: some View {
        ContentView()
            .presentPaywallIfNeeded(
                requiredEntitlementIdentifier: "pro",
                // For a hard paywall, add: presentationMode: .fullScreen
                purchaseCompleted: { customerInfo in
                    print("Purchased: \(customerInfo.entitlements.active.keys)")
                },
                restoreCompleted: { customerInfo in
                    // Dismisses automatically if "pro" is now active.
                    print("Restored: \(customerInfo.entitlements.active.keys)")
                }
            )
    }
}

구매 전에는 넘어갈 방법이 없는 하드 페이월을 만들려면 presentationMode: .fullScreen으로 전체 화면에 표시합니다. 데이터상 하드 페이월은 프리미엄보다 전환율이 훨씬 높아서, 구독이 필요한 콘텐츠에서 흔히 선택합니다.

페이월이 설정되지 않았다면 SDK는 오류를 내지 않고 offering의 package를 나열한 기본 페이월을 표시합니다. 그래서 디자인을 끝내기 전에도 이 코드가 동작하고, V2 페이월을 게시하면 자동으로 그 페이월로 바뀝니다.

시트로 PaywallView 표시하기

예를 들어 "Upgrade" 버튼처럼 필요할 때 페이월을 표시하고 싶다면, 시트에 PaywallView를 표시합니다. 현재 offering의 페이월을 렌더링합니다.

swift
struct SettingsView: View {
    @State private var showPaywall = false

    var body: some View {
        Button("Upgrade to Pro") { showPaywall = true }
            .sheet(isPresented: $showPaywall) {
                // No need to wrap this in a ScrollView, the paywall handles scrolling.
                PaywallView()
            }
    }
}

PaywallView()는 현재 offering을 사용합니다. 특정 offering을 표시하려면 PaywallView(offering: yourOffering)처럼 명시적으로 넘깁니다. 다만 이 경우 A/B 실험 라우팅에서는 제외됩니다.

코드로 닫기 버튼을 추가하지 마세요. displayCloseButton 파라미터는 V2 페이월에서 아무 효과가 없습니다. 닫기 버튼이 필요하면 에디터에서 컴포넌트로 추가하세요.

완료와 닫기 처리하기

구매, 복원, 그리고 사용자가 페이월을 닫으려는 요청에 반응하도록 핸들러를 붙입니다.

swift
.sheet(isPresented: $showPaywall) {
    PaywallView()
        .onPurchaseCompleted { customerInfo in
            // Verify access before unlocking; receiving CustomerInfo is not proof of entitlement.
            if customerInfo.entitlements.active["pro"]?.isActive == true {
                showPaywall = false
            }
        }
        .onRestoreCompleted { customerInfo in
            if customerInfo.entitlements.active["pro"]?.isActive == true {
                showPaywall = false
            }
        }
        .onRequestedDismissal {
            // Fires on close-button tap (or purchase completion).
            showPaywall = false
        }
}
항상 entitlement를 확인하세요. 완료 핸들러가 CustomerInfo를 준다고 해서 그 자체로 사용자에게 자격이 있다는 뜻은 아닙니다. 콘텐츠를 열기 전에 customerInfo.entitlements.active["pro"]?.isActive를 확인하세요. PaywallViewonRequestedDismissal을 씁니다(여기에는 onDismiss 모디파이어가 없습니다).

다른 플랫폼에서 표시하기

같은 대시보드 페이월이 모든 플랫폼에서 렌더링됩니다. 표시 API는 다음과 같습니다.

React Native

tsx
import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui';

const result = await RevenueCatUI.presentPaywallIfNeeded({
  requiredEntitlementIdentifier: 'pro',
});
// result is PURCHASED, RESTORED, CANCELLED, ERROR, or NOT_PRESENTED

Android (Jetpack Compose)

kotlin
// paywallActivityLauncher is a PaywallActivityLauncher created in onCreate.
// Launch the paywall only if the entitlement is missing:
paywallActivityLauncher.launchIfNeeded(requiredEntitlementIdentifier = "pro")

Flutter

dart
import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';

final result = await RevenueCatUI.presentPaywallIfNeeded("pro");
// result is a PaywallResult: notPresented, error, cancelled, purchased, restored
디자인 하나로 모든 플랫폼. 페이월을 대시보드에서 정의하므로, 한 번 디자인하면 iOS, Android, React Native, Flutter에서 네이티브로 렌더링됩니다. 페이월 UI는 Android API 24 이상이 필요합니다.

테스트, A/B, 마무리

테스트

  1. pro가 없는 사용자로 앱을 실행하고, 두 플랜과 강조된 연간 옵션, 체험 문구, "Continue" CTA가 담긴 페이월이 뜨는지 확인합니다.
  2. 샌드박스 구매를 완료하고, 페이월이 닫히며 콘텐츠가 열리는지 확인합니다.
  3. 대시보드에서 페이월을 수정하고 게시한 뒤 다시 실행합니다. 새 빌드 없이 변경이 반영됩니다.

다음은 A/B 테스트

진짜 성과는 테스트에서 나옵니다. 페이월이 offering에 연결되므로, 다른 페이월을 담은 두 번째 offering을 만들어 RevenueCat Experiment로 어느 쪽 전환율이 더 높은지 확인할 수 있습니다. 그런 다음 TargetingPlacements로 대상별로 다른 페이월을 노출할 수 있습니다.

무엇을 만들었나요

데이터 기반 두 플랜 레이아웃을 갖춘 원격 노코드 네이티브 페이월을 만들었습니다. presentPaywallIfNeededPaywallView로 SwiftUI에 표시했고, 모든 플랫폼에서 렌더링하고 실험으로 이어질 준비까지 마쳤습니다.

계속 학습하기