본문 바로가기

개발이야기

무료앱에 광고제거 구독모델(In-App Purchase, ISP) 도입 하기

플러터에서 인앱 결제를 구현하는 방법은 크게 두 가지가 있습니다.

  1. 공식 패키지 (in_app_purchase) 사용: 무료지만, 영수증 검증 서버를 직접 구축해야 하고 구현이 매우 복잡합니다.
  2. RevenueCat 사용: 타사 솔루션이지만, 서버 구축이 필요 없고 구현이 압도적으로 쉬우며, 월 매출 $2,500(약 300만원)까지는 무료입니다.

구독 모델 도입이 처음이라면 RevenueCat을 사용하는 것을 강력 추천합니다. 이를 기준으로 5단계 절차를 상세히 안내해 드립니다.

 

 

1단계: 스토어 판매자 계정 설정 및 상품 등록 (가장 먼저 할 일)

코드를 짜기 전에 애플과 구글에 "나 돈 받을 거다"라고 신고하고 상품을 등록해야 합니다.

🍎 iOS (App Store Connect)

  1. 계약 및 세금: App Store Connect > 비즈니스(Business) 탭에서 '유료 앱 계약(Paid Apps Agreement)'을 체결하고 계좌 정보를 입력합니다. (승인까지 시간이 좀 걸립니다.)
  2. 상품 생성: 앱 선택 > 수익 창출(Monetization) > 구독(Subscriptions) 메뉴로 이동합니다.
  3. 구독 그룹 생성: 예: Premium Access 그룹 생성.
  4. 구독 상품 추가: 예: remove_ads_monthly라는 제품 ID(Product ID)로 '1개월' 상품을 만듭니다. 가격을 설정합니다.

🤖 Android (Google Play Console)

  1. 판매자 계정: Google Play Console > 설정 > 판매자 계정을 만들고 계좌를 연결합니다.
  2. 상품 생성: 앱 선택 > 수익 창출 > 제품 > 구독 메뉴로 이동합니다.
  3. 상품 추가: 예: remove_ads_monthly라는 제품 ID로 상품을 만들고, 기본 요금제(매월 자동 갱신)를 설정합니다.

"앱에 아직 구독이 없습니다. 새 APK를 업로드하세요."라는 메시지가 표시되면 AndroidManifest.xml에 다음 권한을 추가하고 Play Console에 APK를 업로드해야 합니다.

<uses-permission android:name="com.android.vending.BILLING" />

필요한 결제 권한이 있는 APK를 업로드하면 새 구독을 생성할 수 있습니다.

 

2단계: RevenueCat(레비뉴캣) 설정

RevenueCat은 구글/애플 결제 시스템과 플러터 앱 사이의 '중개인' 역할을 합니다.

  1. RevenueCat 가입 및 새 프로젝트 생성.
  2. 앱 추가: 프로젝트 설정에서 iOS(App Store)와 Android(Play Store) 앱을 각각 추가합니다.
    • 이 과정에서 구글/애플의 공유 시크릿 키(Shared Secret) 등이 필요합니다. RevenueCat 가이드가 스크린샷과 함께 아주 친절하게 안내해 줍니다.
  3. Entitlements(권한) 설정:
    • Products: 1단계에서 만든 구글/애플의 Product ID (remove_ads_monthly)를 RevenueCat에 등록하여 연결합니다.
    • Entitlements: 유저가 얻게 될 '권한'의 이름을 정합니다. (예: premium). 이 premium 권한에 방금 등록한 Product들을 연결합니다.
    • 핵심: 코드는 이제 구글/애플 ID 대신 이 premium이라는 권한만 확인하면 됩니다.

 

 

3단계: 플러터 코드 구현

이제 앱에 기능을 넣을 차례입니다.

1. 패키지 추가 (pubspec.yaml)

YAML
 
dependencies:
  purchases_flutter: ^8.0.0 # 최신 버전 확인 필요

2. 초기화 (main.dart) 앱이 시작될 때 RevenueCat을 초기화합니다.

Dart
 
import 'package:purchases_flutter/purchases_flutter.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 플랫폼에 맞는 API 키 설정 (RevenueCat 대시보드에서 확인)
  if (Platform.isAndroid) {
    await Purchases.configure(PurchasesConfiguration("goog_api_key_..."));
  } else if (Platform.isIOS) {
    await Purchases.configure(PurchasesConfiguration("appl_api_key_..."));
  }
  
  runApp(const MyApp());
}

3. 구독 여부 확인 및 광고 숨기기 배너 광고가 있는 위젯에서 현재 유저가 프리미엄 유저인지 확인합니다.

Dart
 
bool _isPremium = false;

@override
void initState() {
  super.initState();
  _checkSubscription();
}

Future<void> _checkSubscription() async {
  try {
    CustomerInfo customerInfo = await Purchases.getCustomerInfo();
    // 'premium'은 2단계에서 설정한 Entitlement 이름
    if (customerInfo.entitlements.all['premium']?.isActive == true) {
      setState(() {
        _isPremium = true; // 광고 제거!
      });
    }
  } catch (e) {
    // 에러 처리
  }
}

// 빌드 메서드에서
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      // 프리미엄이 아닐 때만 배너 표시
      if (!_isPremium) 
        Container(height: 90, child: TopBannerRotator()), 
      
      Expanded(child: MainContent()),
    ],
  );
}

4. 구매 화면 (Paywall) 만들기 유저가 '광고 제거하기' 버튼을 눌렀을 때 실행할 로직입니다.

Dart
 
Future<void> _purchasePro() async {
  try {
    // 판매중인 상품 목록 가져오기
    Offerings offerings = await Purchases.getOfferings();
    
    if (offerings.current != null && offerings.current!.availablePackages.isNotEmpty) {
      // 월간 상품 선택 (예시)
      Package package = offerings.current!.availablePackages.first;
      
      // 결제 요청 창 띄우기
      CustomerInfo customerInfo = await Purchases.purchasePackage(package);
      
      // 결제 성공 시 확인
      if (customerInfo.entitlements.all['premium']?.isActive == true) {
        setState(() {
          _isPremium = true;
        });
        // "구매 감사합니다" 팝업 등
      }
    }
  } catch (e) {
    // 유저가 취소했거나 에러 발생 시
    print(e); 
  }
}

 

 

 

4단계: 필수 UI/UX 요건 (심사 거절 방지)

앱스토어 심사는 구독에 매우 까다롭습니다. 구매 화면(Paywall)에 반드시 다음이 포함되어야 합니다.

  1. 구매 복원(Restore) 버튼: 기기를 바꾼 유저가 이전에 산 내역을 불러오는 버튼이 필수입니다.
  2. Dart
     
    TextButton(
      onPressed: () async {
        try {
          CustomerInfo restoredInfo = await Purchases.restorePurchases();
           if (restoredInfo.entitlements.all['premium']?.isActive == true) {
             setState(() => _isPremium = true);
           }
        } catch (e) { ... }
      },
      child: Text("구매 내역 복원"),
    )
    
  3. 약관 링크: 이용약관(Terms of Use)과 개인정보처리방침(Privacy Policy) 링크가 구매 화면에 보여야 합니다.
  4. 명확한 가격 표시: "월 ₩X,XXX원, 자동 갱신됨" 문구가 명확해야 합니다.

 

 

 

5단계: 테스트 및 배포

  1. 테스터 등록:
    • Android: Play Console > 내부 테스트 트랙에 이메일 등록 (실제 결제 카드를 쓰지만 '테스트용 카드' 옵션을 선택하거나 결제되지 않음).
    • iOS: App Store Connect > TestFlight 사용. 'Sandbox 계정'을 만들어 설정 > App Store 메뉴에서 샌드박스 계정으로 로그인 후 테스트 (실제 결제 안 됨).
  2. 심사 제출: 광고 제거 기능이 제대로 작동하는지 스크린샷이나 설명을 심사 제출 시 메모에 남기면 좋습니다.