본문으로 건너뛰기

인앱 정기결제

문서 정보

  • 작성일: 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 대비 차이, 웹훅 이벤트 라우팅, 멱등성