우리가 해야 할 건 이거다.
Play Console 설정 → 플러그인 설치/동기화 → 결제 코드 → adsRemoved(광고 영구 제거) 연동까지.
< 들어가기 전 알아둬야 할 실수 포인트 >
1. 인앱결제는 “Play 스토어로 설치한 앱”에서만 정상 테스트 됨. ( (Android Studio Run으로 설치한 APK는 결제 플로우가 깨지거나 “상품을 찾을 수 없음”이 자주 뜸)
2. 상품 ID(=productId/SKU) 는 생성 후 변경 불가라 처음부터 원하는 걸로 확정
3. 광고 제거”는 보통 일회성(One-time) 비소모성(non-consumable) 로 설계.
4. Capacitor 공식 가이드는 IAP에 cordova-plugin-purchase 를 쓰는 흐름을 안내.
5. “영구 제거”는 로컬 플래그만 믿지 말고, 앱 시작 시 구매 복원(restore/refresh) 으로 다시 확인하는 루틴이 필요.
★ 우선 프로젝트 구조 먼저 확인하고 가자.
프로젝트/
├─ src/
│ ├─ services/
│ │ ├─ ads/ ← AdMob 관련
│ │ └─ iap.ts ← ⭐ 인앱결제 로직 (핵심)
│ └─ App.tsx ← 앱 시작 시 initIAP 호출
├─ android/
├─ capacitor.config.ts
├─ components/
│ └─ SettingsModal.tsx ← 광고 제거 버튼
Google Play Console 설정 (일회성 상품: 광고 제거)
: One-time product(일회성 제품) 만들고, 활성화, 테스터 계정으로 내부/비공개 테스트 트랙에서 설치해 결제.
▶ 상품 만들기.
- Play Console → (앱 선택) → 수익 창출/Monetize → Products(제품) → One-time products(일회성 제품)
- Create
- Product ID 예시: remove_ads (이 문자열을 코드에서 그대로 사용)
- 이름/설명: “광고 제거”
- 가격 설정 + 국가/지역
- 상태를 Active(활성) 로
** 2025년 이후 “일회성 제품”은 purchase option/offer 같은 구조가 들어옴, 결론은 “광고 제거 권한(entitlement)을 주는 buy 타입 일회성” 하나 만들면 됨.
▶ 테스트 준비.
- Play Console → 설정(Setup) 쪽에서 라이선스 테스트/테스터 계정(결제 테스트 계정)을 등록. 등록하고 꼭 저장 해주기.
- 앱은 내부 테스트(Internal testing) 트랙에 AAB 업로드 → 테스터로 참여 → Play 스토어 링크로 설치
(이 과정을 거쳐야 상품 조회/구매가 안정적으로 된다.)
필수 플러그인 설치 (Capacitor 권장 루트: cordova-plugin-purchase)
▶ 설치 : powerShell에서.
npm install cordova-plugin-purchase
npx cap sync android
>> 이미 프로젝트 파일에 안드로이드 폴더가 있다면 npx cap sync android 로 플러그인/ 네이티브 반영 해주기.
결제 코드 구현(React)
: IAP 서비스 파일 생성 >> src/services/iap.ts
우선 src 폴더에서 services폴더 만들어 둔 곳에 iap.ts 파일 만들어 주자.
// src/services/iap.ts
// cordova-plugin-purchase 는 전역 CdvPurchase 를 주입.
declare global {
interface Window {
CdvPurchase: any;
}
}
export const PRODUCT_REMOVE_ADS = "remove_ads"; // Play Console의 Product ID와 100% 동일해야 함
let initDone = false;
export function initIAP(onAdsRemoved: (v: boolean) => void) {
if (initDone) return;
initDone = true;
const run = () => {
const CdvPurchase = (window as any).CdvPurchase;
const store = CdvPurchase?.store;
if (!store) {
console.warn("[IAP] store not ready, retry...");
setTimeout(run, 300);
return;
}
const { ProductType, Platform } = CdvPurchase;
// < 제품 등록 >
store.register({
id: PRODUCT_REMOVE_ADS,
type: ProductType.NON_CONSUMABLE,
platform: Platform.GOOGLE_PLAY,
});
// < 구매 승인(approval) 시점: 여기서 "권한 부여 + finish"가 핵심 >
store.when(PRODUCT_REMOVE_ADS).approved((p: any) => {
// 1) 권한 부여
localStorage.setItem("ads_removed", "true");
onAdsRemoved(true);
// 2) 트랜잭션 종료(ACK/finish)
// (플러그인이 내부적으로 Play Billing acknowledge/consume를 처리)
p.finish();
});
// 3) 에러 로그
store.error((e: any) => {
console.warn("[IAP] error", JSON.stringify(e ?? {}));
});
// store.initialize + store.update 를 해줘야 상품/영수증 상태가 들어옴.
store.initialize([Platform.GOOGLE_PLAY]);
store.update();
console.log("[IAP] init done");
};
// ⚠️ deviceready 기준으로 단 한 번만 실행
document.addEventListener("deviceready", run, { once: true });
}
// 결제 호출 - 가장 중요한 부분
export async function buyRemoveAds() {
const store = (window as any).CdvPurchase?.store;
if (!store) throw new Error("IAP store not ready");
// 1) product 가져오기
const product =
typeof store.get === "function"
? store.get(PRODUCT_REMOVE_ADS)
: null;
if (!product) {
throw new Error("Product not loaded yet");
}
// 2) 핵심: Offer 가져오기
const offer =
(typeof product.getOffer === "function"
? product.getOffer()
: null) ||
product.offers?.[0] ||
null;
if (!offer) {
throw new Error("Offer not found");
}
// 3) Offer로 주문
await store.order(offer);
}
★ 여기서 포인트는 ★
1. RODUCT_REMOVE_ADS 는 Play Console의 Product ID와 완전히 동일 해야 한다는 것.
2. approved → finish() 흐름은 결제 완료 후 권한 부여/정리에 해당. (원타임 구매 라이프사이클에서 앱이 상태 전이를 제대로 처리해야 함)
3. cordova-plugin-purchase v13에서는 order()에 SKU나 product를 넣으면 안 된다. 반드시 Offer 객체를 넣어야 한다.
기존 “광고 제거 버튼”과 연결 + adsRemoved 전역 상태 연동
: 이미 adRemoved 토글로 배너 show/hide 구조있으니까 “토글 UI”는 그대로 두고,
‘구매 성공’이 토글을 true로 바꾸는 트리거가 되게만 연결.
▶ App 진입 시 IAP 초기화 (파일 위치 : App.tsx 또는 최상위 Provider)
import { initIAP } from "./src/services/iap";
// ✅ IAP init (app start)
useEffect(() => {
const run = () => {
initIAP((removed) => {
if (removed) {
setSettings((s: any) => ({ ...s, adsRemoved: true }));
}
});
};
▶ SettingsModal (파일 위치 src/components/SettingsModal.tsx) - “광고 제거” 버튼
<button
onClick={async () => {
try {
await buyRemoveAds();
// 여기서는 adsRemoved를 직접 바꾸지 않음
// → 결제 승인 시 App.tsx의 initIAP 콜백에서만 변경됨
} catch (e: any) {
alert(e?.message ?? "결제를 시작할 수 없어요.");
}
}}
>
</button>
마지막으로 코드를 수정했을 땐 언제나 습관처럼 하는 순서.
1. PowerShell 에서 우선 해주기.
npm run build
npx cap sync android
2. 만약 AAB 파일 뽑을 거라면 Android Studio에서 versionCode 올려주기.
Build.gradle 찾아서 versionCode, versionName 1씩 올려주기.

3. 안드로이드 스튜디오 실행시켜서 빌드 해주기.
** 놓치면 오류가 찾아가는 것들.
1. AAB 파일 제대로 넣어뒀는데 일회성 상품 안만들어진다? >> powershell 켜고 purchase 플러그인 설치.
2. 기껏 구글 콘솔 플레이에 올려두고 Play 스토어 링크로 설치 안하고 android studio Run으로 실행? 의미 없음.
3. 구글 콘솔 플레이에 올려두고 Play 스토어에서 다운받아 설치했는데 뭔가 수정한 것들이 안바껴있다?
>> 앱 버전 확인. (build.gradle 들어가서 versionCode와 versionName 설정해주고 Play스토어에 업데이트한 버전으로 올라와있는지 체크하기. 이전 버전이라면 핸드폰에 있는 어플 삭제, 설정 > 앱 > 구글플레이스토어, 구글플레이서비스 캐시삭제, 구글플레이 종료후 다시 테스터로 들어가보기.)
Product not registered: null 오류가 뜬다면.
원인은 간단하다.
- store.order("remove_ads")
- store.order(product)
해결은 위에서 처럼 store.order(offer); 로 작업.
팝업이 안뜬다면
- store 초기화 전 order 호출
- deviceready 이전 실행
→ initIAP 구조 확인하기.
'게임' 카테고리의 다른 글
| Capacitor + React 앱 ) 인앱결제(광고 제거) 구조 정리 & 재설치/기기 변경 복원 설계 가이드 (0) | 2026.01.22 |
|---|---|
| Android Studio ) 키스토어 설정, AAB 파일 생성하기. (feat. alias 잊었을 때 확인하는 방법) (0) | 2026.01.19 |
| Capacitor + React(Vite) Android ) AdMob 전역 배너 구현 & 토글 오류 해결 기록 (1) | 2026.01.16 |
| AIStudio ) PowerShell로 Android( Capacitor ) 세팅 끝내기 (1) | 2026.01.16 |
| Capacitor ) AdMob 광고 추가 + 광고 제거(영구) 인앱결제 구현하기 (초보자용) (0) | 2026.01.12 |
