본문으로 건너뛰기

인앱 정기결제

문서 정보​

  • 작성일: 2026-04-20
  • 버전: v2.3.0

TL;DR

IAP 정기결제는 Apple/Google 스토어에서 사용자가 구독하는 월/연 단위 멤버십입니다. 결제·갱신·환불은 모두 스토어가 담당하고, 서버는 RevenueCat Webhook으로 결과만 반영합니다. 기존 Portone 구독과 혜택은 동일하지만 재화(리워드 포인트) 자동차감은 지원하지 않습니다.


IAP 구독이란 (사용자 관점)​

사용자는 앱의 구독 탭에서 PLUS 또는 PRO 멤버십을 선택하고, Apple/Google 결제 화면에서 결제합니다. 이후 매월 또는 매년 자동으로 갱신되며, 해지 시에는 스토어 앱에서 직접 자동갱신을 끕니다.

사용자 여정:

  1. 앱 구독 탭 → 플랜 선택 (PLUS/PRO, 월/연)
  2. Apple/Google 결제 화면에서 결제 완료
  3. 서버가 결제 완료 Webhook을 받아 멤버십 활성화
  4. 매 주기마다 스토어가 자동 갱신 → 서버가 Webhook으로 갱신 반영
  5. 사용자가 스토어에서 자동갱신 해지 → 현재 기간 종료 후 만료

Portone 구독과의 차이​

CS에서 가장 자주 받는 질문: "왜 스토어 구독이 다른가요?"

항목Portone 구독IAP 정기결제 (Apple/Google)
결제수단 보관 주체서버 (빌링키 저장)Apple/Google
자동갱신 처리서버 스케줄러 (매일 자정 KST)Apple/Google 자동 갱신
재화(포인트) 자동차감지원 (갱신 시 리워드 포인트 차감 후 결제)미지원
환불 방법서버에서 직접 환불 API 호출 가능스토어에 직접 요청, 서버는 결과만 수신
해지 방법서버 API 호출스토어 앱에서 자동갱신 끄기
플랜 변경서버에서 업그레이드/다운그레이드 지원현재 미지원 (하단 참고)
왜 재화 자동차감이 안 되나요?

Apple/Google이 갱신 결제를 자체 서버에서 처리하기 때문에, 갱신 시점에 사내 포인트 차감 로직을 끼워넣을 방법이 없습니다. 스토어 약관상 결제 흐름을 서버가 중간에 개입할 수 없습니다. 멤버십 혜택(광고 제거, 고화질, 마이콕 등)은 동일하게 제공됩니다.


6가지 사용자 행동과 서버 반응​

1. 최초 구독 (INITIAL_PURCHASE)​

사용자가 처음 구독을 시작하면 스토어가 결제를 처리하고 RevenueCat이 서버로 알립니다.

서버 처리:

  • 신규 구독 레코드 생성, 상태 ACTIVE
  • 멤버십 결제 내역 기록
  • 갱신일을 paymentCycle 기준으로 계산 (MONTHLY: +1개월, YEARLY: +12개월)
  • 멤버십 권한(Entitlement) 활성화
  • 앱에 SSE 실시간 알림 + 푸시 알림 발송

2. 자동 갱신 성공 (RENEWAL)​

구독 기간이 끝나면 스토어가 자동으로 다음 기간을 결제하고 서버에 알립니다.

서버 처리:

  • 갱신 결제 내역 추가 기록
  • 구독 갱신일 연장
  • 멤버십 권한 유효기간 연장
  • 앱에 SSE 실시간 알림 + 푸시 알림 발송

3. 환불 요청 (REFUND)​

사용자가 Apple/Google에 환불을 요청하고 스토어가 승인하면 서버에 통보됩니다.

서버는 환불을 막을 수 없습니다

사용자가 스토어에서 환불을 받으면, 서버는 그 결과를 통보받을 뿐입니다. 환불을 거부하거나 되돌릴 수 없습니다.

서버 처리:

  • 결제 상태를 REFUNDED로 변경
  • 구독 상태를 CANCELED로 변경, 즉시 만료
  • 부당환불 여부 자동 판정 (결제 후 7일 초과 또는 혜택 소비 후 환불)
  • 부당환불로 판정되면 strike +1 (라이프타임 3회 누적 시 블랙리스트)
  • 앱에 SSE 실시간 알림 + 푸시 알림 발송

환불 정책 상세 → IAP-REFUND-POLICY.md

4. 자동갱신 해지 (CANCELLATION)​

사용자가 스토어에서 자동갱신을 끄면 발생합니다. 현재 구독 기간은 그대로 유지됩니다.

서버 처리:

  • 구독 상태를 PENDING_CANCEL로 변경
  • 현재 구독 기간 종료까지는 멤버십 혜택 정상 유지
  • 앱에 SSE 실시간 알림 + 푸시 알림 발송
CS 안내 포인트

"자동갱신 해지를 했는데 멤버십이 바로 끊겼나요?" → 아닙니다. PENDING_CANCEL은 현재 기간은 정상 사용하고, 다음 갱신일에 갱신이 안 되는 상태입니다. EXPIRATION 이벤트가 올 때 실제로 만료됩니다.

5. 만료 (EXPIRATION)​

자동갱신 해지 후 구독 기간이 실제로 끝나면 발생합니다.

서버 처리:

  • 구독 상태를 CANCELED로 변경, 만료일 기록
  • 멤버십 혜택 종료
  • 앱에 SSE 실시간 알림 + 푸시 알림 발송

6. 플랜 업그레이드 — 현재 미지원​

현재 미지원

IAP 구독의 플랜 변경(예: MONTHLY → YEARLY, PLUS → PRO)은 현재 서버에서 지원하지 않습니다. 스토어가 플랜 전환과 proration을 별도로 처리하며, 서버 측 Webhook 처리 로직이 아직 구현되지 않았습니다. 특히 서버의 웹/Portone 즉시 업그레이드는 남은 일수를 계산해 차액을 청구하는 정책이므로, IAP 구독의 중간 업그레이드와 동일하게 취급할 수 없습니다.

서버는 IAP 구독에서 즉시 플랜 변경(IMMEDIATE) 요청을 차단하고, 클라이언트에 CHANGE_PLAN_IMMEDIATE 액션도 노출하지 않습니다. 추후 지원 시 별도 PR/ADR로 스토어 플랜 전환 이벤트 처리와 가격 정책을 먼저 정의해야 합니다.


결제 주기와 갱신일​

주기값갱신 간격
월간MONTHLY구독일로부터 +1개월
연간YEARLY구독일로부터 +12개월

서버는 Portone 구독과 IAP 구독 모두 같은 계산식으로 nextBillingDate를 설정합니다 (BillingPeriodVO.createFromPaymentCycle). 가입 시점 기준 KST 자정에 맞춰 다음 결제일을 산정합니다.

웹(Portone)과 모바일(IAP) 사이클 일관성​

같은 날 가입한 사용자는 결제 채널과 무관하게 갱신 날짜(YYYY-MM-DD)가 정확히 일치합니다.

구독 채널nextBillingDate 계산 기준예시 (2026-04-21 가입, MONTHLY)
Portone (웹)KST 자정 기준 +N개월2026-05-21
IAP (모바일)KST 자정 기준 +N개월2026-05-21
사용자 UI에 날짜만 노출하는 이유

웹과 모바일 모두 날짜 레벨에선 완전히 일치하지만, 시각(시/분/초) 레벨까지 보면:

  • Portone은 우리 서버 스케줄러가 KST 자정에 갱신 결제 실행
  • IAP는 Apple/Google이 사용자가 최초 구독한 시각(UTC)으로 갱신 결제 실행

따라서 앱/웹 UI에서는 YYYY-MM-DD만 표시하고 시각은 숨기는 것을 권장합니다. Netflix, Spotify, YouTube Premium 등 대부분의 서비스가 이 방식을 씁니다.

"만료-갱신 갭" 방지 (Entitlement validUntil 동기화)

접근 권한을 제어하는 Entitlement.validUntil은 두 가지 값 중 하나로 설정됩니다:

  1. 우선 사용: RevenueCat webhook payload의 expiration_at_ms (스토어가 결정한 실제 만료 시각)
  2. Fallback: 필드가 없을 때만 nextBillingDate + 24시간 버퍼 (KST 자정 기준 계산과 스토어 UTC 시각 최대 오차 커버)

따라서 사용자는 웹/모바일 어디서든 "서버 만료는 됐는데 스토어 갱신은 아직"인 상황을 겪지 않습니다. Subscription.nextBillingDate(UI에 표시되는 값)는 계속 BillingPeriodVO 계산값을 유지하므로 웹/모바일 날짜 일치도 그대로입니다.

갱신일 정확도

실제 스토어 결제일과 서버의 nextBillingDate 사이에 수 분~수 시간의 시각 차이가 생길 수 있습니다. Webhook 전달 지연과 스토어 시각(UTC) vs 서버 자정(KST) 기준 차이 때문입니다. 날짜 레벨 정확도는 보장되므로 정상입니다.


알림과 실시간 반영​

멤버십 상태가 변경될 때마다 두 채널로 사용자에게 알립니다.

채널방식목적
SSE앱이 열려있는 경우 즉시 UI 갱신실시간 상태 반영
푸시 알림앱이 백그라운드/종료 상태일 때결제/갱신/만료 안내

"결제됐는데 앱에 반영이 안 돼요" CS 대응 체크리스트:

  1. 인터넷 연결 상태 확인 (SSE는 연결이 끊기면 즉시 반영 안 됨)
  2. 앱 재실행 (재실행 시 최신 멤버십 상태 재조회)
  3. RevenueCat → 서버 Webhook 지연 가능성 (보통 수 초~수 분)
  4. 어드민 패널에서 해당 유저의 구독 상태 직접 확인

환불과 블랙리스트 정책​

IAP 정기결제 환불은 부당환불 횟수(strike)로 추적합니다. 라이프타임 3회 누적 시 블랙리스트로 전환됩니다.

상세 정책 → IAP-REFUND-POLICY.md


개발자 참조 (Appendix)​

RevenueCat 이벤트 → 서버 커맨드 매핑​

RevenueCat 이벤트조건처리 커맨드strike 영향
INITIAL_PURCHASEPassMembership SKUProcessIapSubscriptionInitialPurchaseCommand없음
RENEWALPassMembershipProcessIapSubscriptionRenewalCommand없음
REFUNDPassMembershipProcessIapSubscriptionRefundCommand부당 판정 시 +1
CANCELLATIONPassMembershipProcessIapSubscriptionCancellationCommand없음
EXPIRATIONPassMembershipProcessIapSubscriptionExpirationCommand없음
INITIAL_PURCHASEGrainPlan SKUProcessIapPurchaseCommand없음
REFUNDGrainPlanProcessIapRefundCommanddebt 누적

paymentChannel 값​

값설명
PORTONE기존 Portone 빌링키 구독
IAP_APPLEApple App Store 인앱 구독
IAP_GOOGLEGoogle Play Store 인앱 구독

관련 ADR​

관련 문서​


변경 이력​

버전날짜변경 내용
v2.3.02026-05-06IAP 즉시 업그레이드 차단 정책 추가 — 웹/Portone 차액 청구 정책과 IAP proration 차이를 명시
v2.2.02026-04-20Entitlement validUntil 스토어 동기화 설명 추가 — expiration_at_ms 우선 + 24h fallback 버퍼로 만료-갱신 갭 구조적 제거 명시
v2.1.02026-04-20"결제 주기와 갱신일" 섹션 확장 — 웹(Portone) vs 모바일(IAP) 사이클 일관성 설명, 드물게 발생 가능한 "만료-갱신 갭"과 CS 대응 추가
v2.0.02026-04-20전면 재작성 — 개발자 매뉴얼 톤에서 CS 친화 시나리오 중심으로 재구성. 사용자 행동-서버 반응 6가지 시나리오, Portone 비교표, 알림 체크리스트, 개발자 참조 appendix로 분리
v1.0.02026-04-20초기 문서 작성 — IAP 정기결제 개요, Portone 대비 차이, 웹훅 이벤트 라우팅, 멱등성