Flutter 인앱 결제 및 페이월 개요
0:02:00RevenueCat Flutter SDK Codelab에 오신 것을 환영합니다!
목표: 이 Codelab에서는 RevenueCat Flutter SDK를 새 Flutter 애플리케이션에 연동하여 상품 Offering을 가져오고 사전 구축된 네이티브 페이월 UI를 표시하는 방법을 배웁니다.
구축할 내용:
로딩 인디케이터를 표시하고 RevenueCat에서 구성된 "Offering"을 가져온 다음 PaywallView를 표시하는 간단한 단일 화면 앱입니다.
사전 요구사항
시작하기 전에 다음이 설정되어 있어야 합니다:
- Flutter SDK: Flutter 개발 환경이 준비되어 있어야 합니다.
- RevenueCat 계정: revenuecat.com에서 무료 계정이 필요합니다.
- App Store / Play Store 구성:
- 실제 기기 또는 구성된 에뮬레이터: 인앱 결제 테스트용입니다.
이 Codelab을 완료하면, RevenueCat의 Flutter SDK를 사용하여 Flutter 앱에서 인앱 결제를 성공적으로 구현하고 동적 페이월을 표시할 수 있습니다.
RevenueCat SDK 가져오기
0:05:00인앱 결제를 구현하기 전에, 먼저 기존 프로젝트 또는 새 프로젝트에 RevenueCat SDK를 가져와야 합니다. 시작하려면 pubspec.yaml 파일에 다음 의존성을 추가하세요:
GitHub에서 최신 릴리스 버전을 확인할 수 있습니다.
dependencies:
purchases_flutter: 9.2.3
purchases_ui_flutter: 9.2.3아래 이미지와 같이 해당 의존성을 추가했다면 "Pub get" 버튼을 클릭하면 필요한 의존성이 자동으로 다운로드됩니다.
이제 RevenueCat SDK를 초기화해 보겠습니다. main.dart 파일에서 수행합니다.
-
lib/main.dart를 열고 전체 내용을 다음 코드로 교체합니다. - API 키를 붙여넣으세요
_androidApiKey및_iosApiKey상수에 붙여넣습니다.
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
import 'dart:io' show Platform;
// --- 여기에 REVENUECAT 키를 붙여넣으세요 ---
const String _androidApiKey = "goog_YOUR_KEY_HERE";
const String _iosApiKey = "appl_YOUR_KEY_HERE";
void main() async {
// 비동기 플러그인 호출 전에 Flutter 위젯이 초기화되었는지 확인
WidgetsFlutterBinding.ensureInitialized();
// 앱을 실행하기 전에 RevenueCat SDK 초기화
await _initializeRevenueCat();
runApp(const MyApp());
}
Future<void> _initializeRevenueCat() async {
// 개발용 디버그 로그 활성화
await Purchases.setLogLevel(LogLevel.debug);
PurchasesConfiguration? configuration;
if (Platform.isAndroid) {
configuration = PurchasesConfiguration(_androidApiKey);
} else if (Platform.isIOS) {
configuration = PurchasesConfiguration(_iosApiKey);
} else {
print("RevenueCat이 이 플랫폼에 대해 구성되지 않았습니다.");
return;
}
try {
await Purchases.configure(configuration);
print("RevenueCat이 성공적으로 구성되었습니다!");
} catch (e) {
print("RevenueCat 구성 오류: $e");
}
}
// ... 나머지 코드는 다음 단계에서 작성합니다 ...async main 함수를 만들었습니다. 이를 통해 _initializeRevenueCat 함수를 await하여 UI가 빌드되기 전에 RevenueCat SDK가 구성되고 준비되었음을 보장합니다. 이것이 시작부터 사용 가능해야 하는 플러그인을 초기화하는 올바른 방법입니다.
이것으로 구현의 50%를 완료했습니다!
Entitlement 검증하기
0:03:00이제 사용자 Entitlement를 검증하는 방법을 알아보겠습니다.
앞서 언급했듯이, Entitlement는 사용자가 구매 후 잠금 해제하는 접근 수준 또는 기능을 나타냅니다. 이를 통해 광고 배너를 표시할지 또는 프리미엄 접근 권한을 부여할지 등을 결정할 수 있습니다.
아래 코드 스니펫을 사용하여 사용자가 활성 Entitlement를 가지고 있는지 쉽게 확인할 수 있습니다:
const ENTITLEMENT_IDENTIFIER = ".."; // RevenueCat 대시보드에서 특정 entitlement identifier를 가져오세요
final customerInfo = await Purchases.getCustomerInfo();
final isEntitled = customerInfo.entitlements.active[ENTITLEMENT_IDENTIFIER]?.isActive;사용자가 특정 Entitlement를 가지고 있는지 확인한 후, 앱의 비즈니스 모델에 따라 진행 방법을 결정할 수 있습니다.
예를 들어, 앱이 광고 지원 방식이라면 AdMob 배너를 표시하거나 숨길 수 있습니다. 또는 사용자가 고급 기능이나 콘텐츠를 잠금 해제할 수 있도록 페이월이나 구매 다이얼로그를 표시할 수도 있습니다.
다음은 해당 로직을 구현하는 예시입니다:
if (isEntitled == true) {
// 사용자가 이 entitlement에 접근 권한이 있으면 배너를 표시할 필요가 없습니다.
} else {
// 여기에 배너 UI를 표시하거나 페이월을 표시합니다
..
}인앱 결제 구현하기
0:04:00이제 광고 없는 경험을 제공하기 위한 인앱 결제를 구현해 보겠습니다. 시작하려면 먼저 RevenueCat 대시보드에서 관련 상품 정보를 가져와야 합니다. 이 상품 데이터는 사용자에게 구매 옵션을 제시하는 데 사용됩니다.
아래 예시와 같이 Purchases.sharedInstance.awaitGetProducts()를 호출하여 사용 가능한 상품을 가져올 수 있습니다:
// RevenueCat 서버에서 상품 정보를 가져옵니다
final List<StoreProduct> products = await Purchases.getProducts(['paywall_tester.subs']);
// 인앱 결제를 진행합니다
final purchaseResult = await Purchases.purchaseStoreProduct(products.first);paywall_tester.subs:weekly, paywall_tester.subs:monthly, paywall_tester.subs:yearly와 같이 여러 상품 변형을 제공하는 경우, productIds 필드에 기본 상품 식별자인 paywall_tester.subs를 사용하여 상품 검색을 단순화할 수 있습니다. 이렇게 하면 RevenueCat이 모든 관련 상품 변형을 목록으로 가져와 페이월 UI에서 동적으로 표시할 수 있습니다.
상품 데이터를 가져온 후, Purchases.sharedInstance.awaitPurchase(product)를 호출하여 인앱 결제 흐름을 시작할 수 있습니다. 이렇게 하면 자동으로 Google Play 결제 다이얼로그가 트리거되어 사용자가 앱 내에서 거래를 완료할 수 있습니다.
이것으로 단 몇 줄의 코드만으로 완전한 기능을 갖춘 인앱 결제 흐름을 통합했습니다. 영수증, 스토어 API 또는 구매 검증을 수동으로 처리하는 복잡함을 다룰 필요가 없습니다.
전체 예시 코드는 다음과 같습니다:
import 'package:flutter/services.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
/// ID로 특정 상품을 가져와 구매 흐름을 시작합니다.
///
/// 이 함수는 상품을 찾을 수 없거나, 사용자가 구매를 취소하거나,
/// 다른 스토어 오류와 같은 잠재적 오류를 처리합니다.
Future<void> purchaseProduct() async {
// 1. 가져올 상품 식별자를 정의합니다.
const String productId = 'paywall_tester.subs';
try {
// 2. RevenueCat에서 StoreProduct를 가져옵니다
print('상품 가져오는 중...');
final List<StoreProduct> products = await Purchases.getProducts([productId]);
// 3. 상품 목록이 비어 있지 않은지 확인합니다
if (products.isEmpty) {
print('오류: 상품을 찾을 수 없습니다. ID와 RevenueCat 설정을 확인하세요.');
// 선택적으로 사용자에게 오류 메시지를 표시합니다
return;
}
final StoreProduct productToPurchase = products.first;
print('상품 찾음: ${productToPurchase.title}. 구매 시작 중...');
// 4. 구매 흐름을 시작합니다
final PurchaseResult purchaseResult = await Purchases.purchaseStoreProduct(productToPurchase);
// 5. entitlement를 확인하여 구매가 성공했는지 확인합니다
// "your_premium_entitlement"를 RevenueCat의 실제 entitlement identifier로 교체하세요
if (purchaseResult.customerInfo.entitlements.all["your_premium_entitlement"]?.isActive ?? false) {
print('구매 성공! 사용자가 이제 프리미엄 접근 권한을 갖게 되었습니다.');
// 프리미엄 콘텐츠에 대한 접근 권한을 부여합니다
} else {
print('구매가 완료되었지만 entitlement가 활성화되지 않았습니다.');
}
} on PlatformException catch (e) {
// 6. 잠재적 오류를 처리합니다
final PurchasesErrorCode error = PurchasesErrorHelper.getErrorCode(e);
if (error == PurchasesErrorCode.purchaseCancelledError) {
print('사용자가 구매를 취소했습니다.');
} else {
print('구매 실패 오류: ${e.message}');
}
} catch (e) {
print('예상치 못한 오류가 발생했습니다: $e');
}
}페이월 구현하기
0:07:00이제 Dart에서 Paywall을 구현할 차례입니다.
1단계: 앱 셸 만들기
이제 기본 Flutter 앱 구조를 추가해 보겠습니다. 여기에는 MyApp 위젯과 최종적으로 페이월을 보유할 stateful MyHomePage 위젯이 포함됩니다.
lib/main.dart 파일에 다음 코드를 추가하세요:
// lib/main.dart (계속)
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'RevenueCat Paywall Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'RevenueCat Paywall Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
// ... State 클래스는 다음 단계에서 작성합니다 ...2단계: Offering 가져오기 및 상태 관리
이것이 핵심 로직입니다. _MyHomePageState에서 RevenueCat으로부터 Offering을 가져오고 로딩 및 오류 상태를 관리합니다.
// ... State 클래스는... 주석을 아래의 전체 _MyHomePageState 클래스로 교체하세요:
// lib/main.dart (계속)
class _MyHomePageState extends State<MyHomePage> {
// 상태 변수
Offering? _offering;
bool _isLoading = true;
String? _errorMessage;
@override
void initState() {
super.initState();
// 페이지가 로드되자마자 offering을 가져옵니다
_loadOfferings();
}
Future<void> _loadOfferings() async {
// setState를 호출하기 전에 위젯이 여전히 마운트되어 있는지 확인해야 합니다
if (!mounted) return;
setState(() {
_isLoading = true;
_errorMessage = null; // 이전 오류를 지웁니다
});
try {
// RevenueCat에서 현재 offering을 가져옵니다
final offerings = await Purchases.getOfferings();
// 비동기 호출 후 위젯이 여전히 마운트되어 있는지 확인합니다
if (!mounted) return;
setState(() {
_offering = offerings.current;
_isLoading = false;
if (_offering == null) {
_errorMessage = "현재 offering을 찾을 수 없습니다. RevenueCat 대시보드를 확인하세요.";
}
});
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
_errorMessage = "offering 로드 실패: ${e.toString()}";
});
}
}
// build 메서드는 마지막 단계에서 작성합니다...
@override
Widget build(BuildContext context) {
// 지금은 플레이스홀더입니다
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: const Center(child: Text("거의 다 됐습니다!")),
);
}
}- 스피너를 표시하기 위한
_isLoading, 오류를 표시하기 위한_errorMessage, 페이월의 데이터를 보유하기 위한_offering이라는 세 가지 상태 변수를 만들었습니다. -
initState에서 데이터 가져오기 프로세스를 즉시 시작하기 위해_loadOfferings()를 호출했습니다. -
_loadOfferings메서드는Purchases.getOfferings()호출을try...catch블록으로 감쌉니다. 로더를 표시/숨기고 가져온 offering 또는 오류 메시지를 저장하도록 상태를 업데이트합니다.
3단계: PaywallView 표시하기
마지막으로, 상태 변수에 따라 올바른 UI를 조건부로 표시하도록 build 메서드를 업데이트해 보겠습니다. Offering이 성공적으로 로드되면 PaywallView를 표시합니다.
_MyHomePageState의 build 메서드를 이 완전한 버전으로 교체하세요:
// lib/main.dart (_MyHomePageState 내)
@override
Widget build(BuildContext context) {
Widget content;
// offering을 가져오는 동안 로더를 표시합니다
if (_isLoading) {
content = const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text("페이월 로딩 중..."),
],
);
}
// 문제가 발생하면 오류 메시지를 표시합니다
else if (_errorMessage != null) {
content = Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
_errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red, fontSize: 16),
),
);
}
// offering이 로드되면 PaywallView를 표시합니다
else if (_offering != null) {
content = PaywallView(
offering: _offering!,
onDismiss: () => Navigator.pop(context),
onPurchaseCompleted: (customerInfo) {
print("entitlement에 대한 구매가 완료되었습니다: ${customerInfo.entitlements.active.keys.first}");
// 여기서 닫거나 다른 곳으로 이동할 수 있습니다
},
onPurchaseError: (error) {
print("구매 오류: ${error.message}");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("구매 실패: ${error.message}")),
);
},
onRestoreCompleted: (customerInfo) {
print("복원 완료. 활성 entitlement: ${customerInfo.entitlements.active.length}");
if (customerInfo.entitlements.active.isNotEmpty) {
Navigator.pop(context);
}
},
);
}
// 가능성이 낮은 경우를 위한 대비책
else {
content = const Text("사용 가능한 offering이 없습니다.");
}
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: content,
),
);
}완료되었습니다!
이제 기기나 에뮬레이터에서 애플리케이션을 실행하세요.
flutter run- 스피너와 함께 "페이월 로딩 중..." 메시지가 표시됩니다.
- 그 다음 아래 이미지와 같이 완전히 작동하는 네이티브 페이월 UI가 표시됩니다.
문제 해결
페이월이 표시되지 않으면 디버그 콘솔을 확인하고 다음과 같은 일반적인 문제를 검토하세요: "현재 offering을 찾을 수 없습니다": 가장 일반적인 오류입니다. RevenueCat 대시보드에 "current" offering이 없다는 의미입니다. Offerings로 이동하여 offering을 선택하고 "current"로 표시되어 있는지 확인하세요. 잘못된 API 키: 테스트 중인 플랫폼에 대한 올바른 public API 키를 복사했는지 다시 확인하세요. 상품이 구성되지 않음: 스토어 콘솔의 인앱 상품이 올바르게 설정되어 있고 RevenueCat에서 entitlement 및 offering과 연결되어 있는지 확인하세요. Sandbox/테스트 사용자 문제: 샌드박스 테스터 계정(iOS)으로 로그인했거나 이메일이 라이선스 테스터로 추가되었는지(Android) 확인하세요.
구성 완료입니다! 이제 사용자가 필요한 Entitlement를 가지고 있지 않을 때마다 Paywall Editor에서 구성한 것과 동일한 디자인으로 페이월을 표시할 수 있습니다.
Codelab: RevenueCat Google Play 연동 (페이월 생성)에서 이미 보셨듯이, 페이월 시스템은 서버 기반 UI로 구축되어 있습니다. 이는 앱 업데이트를 푸시하거나 리뷰 프로세스를 거치지 않고도 대시보드에서 직접 페이월의 콘텐츠와 디자인을 동적으로 업데이트할 수 있다는 것을 의미합니다.
마무리
이 Codelab에서는 RevenueCat의 Flutter SDK를 연동하고, 인앱 결제를 구현하고, Flutter에서 페이월을 구축하는 방법을 배웠습니다. 이제 앱을 출시하고 더 많은 수익을 올릴 시간입니다!
아래 리소스를 통해 RevenueCat SDK 사용에 대해 더 자세히 알아볼 수 있습니다:
- 제품 튜토리얼: RevenueCat을 시작하고 최대한 활용하는 데 도움이 되는 비디오 튜토리얼입니다.