R8 최적화가 결제 플러그인을 지웠다
Play Console 요구대로 R8 을 켰더니 Cordova 결제 플러그인 등록부가 지워졌다. 앱 결제가 열흘 동안 0건이었고, keep 규칙 한 줄로 고쳤다.
안드로이드 인앱결제가 9월 1일부터 10일까지 한 건도 들어오지 않았다. 나는 그걸 열흘째 되는 날 밤에 알았다.
오픽 말하기 연습 앱의 결제는 대부분 이 인앱결제에서 나온다. 전체 기간 결제 54건 중 40건, 74%가 앱이다. 웹에도 결제 수단이 있지만 주력은 앱이다.
켜라고 해서 켰다
Play Console 이 DEX 코드 최적화를 요구했다. 요구사항으로 올라와 있었고, 기한이 붙어 있었다.
8월 31일에 minifyEnabled 를 켰다. 9월 1일에 shrinkResources 를 켜고, 최적화 옵션을 조정하고, 라이트모드 릴리스까지 연달아 올렸다. 9월 2일에 프로덕션 심사를 통과했다.
빌드는 전부 성공했다. 경고도 없었다.
결제 테스트만 빼고 다 해봤다
승인된 뒤에 앱을 켜서 확인했다. 로그인, 녹음, 채점, 히스토리. 다 됐다.
결제는 안 눌러봤다. 내부 테스트 트랙에 먼저 올릴 수도 있었는데 그것도 안 하고 프로덕션으로 바로 갔다.
왜 그랬는지는 분명하다. 이 앱의 코드는 대부분 AI 에이전트로 작성하고 수정한다. 그동안 문제가 없었다. 그래서 이번에도 없을 거라고 생각했다. 빌드 설정을 바꾼 릴리스인데도 그렇게 생각했다.
에이전트가 잘못한 건 아니었다. minifyEnabled 와 shrinkResources 는 Google 이 켜라고 한 옵션이고, 켜는 방법도 문서에 있는 그대로다.
열흘 동안 아무 신호도 없었다
두 가지가 겹쳤다.
하나는 실패가 클라이언트에서 끝났다는 것이다. 결제 실패는 서버를 부르기 전에 끝난다. 스토어 초기화가 안 되면 상품을 못 불러오고, 상품이 없으면 구매 요청 자체가 나가지 않는다. 서버에는 요청이 아예 도착하지 않는다. Vercel 로그에도 Sentry 에도 남을 것이 없었다.
또 하나는 에러 핸들러였다. 실제 에러를 사용자 취소로 분류하고 있었다. 플러그인 에러 코드 세 개를 전부 "취소 계열"로 묶어두고 조용히 무시하고 있었다.
6777001 ERR_SETUP 스토어 초기화 실패 → 취소로 처리됨
6777002 ERR_LOAD 상품 로드 실패 → 취소로 처리됨
6777006 ERR_CANCELLED 진짜 사용자 취소실제 에러 메시지는 Init failed - Class not found 였는데, 이 핸들러 때문에 열흘 동안 아무 데도 기록되지 않았다. 화면에는 alert() 하나가 떴다 사라졌다.
웹 결제는 정말로 늘고 있었다
두 달 전에 시작한 SEO 가 효과를 보기 시작한 시점이었다. 데스크톱 유입이 늘었고 웹 결제가 늘었다. 8월 3건에서 9월 9건. 세 배다.
그래서 대시보드가 이상해 보이지 않았다. 결제 채널이 웹으로 옮겨간 것처럼 읽혔다.
실제로는 웹 결제가 늘어난 것과 앱 결제가 사라진 것이 같은 기간에 겹쳤을 뿐이고, 두 일은 서로 아무 관계가 없었다.
9월 10일 밤, 웹 결제 비중을 들여다보다가 질문이 하나 남았다. 꾸준히 있던 안드로이드 결제는 왜 한 건도 없나.
폰을 꺼내서 직접 눌러봤다. "처리 중…" 에서 멈췄다. 나갔다 다시 들어가니 상품 정보를 불러올 수 없다는 문구가 떴는데, App Store Connect 를 확인하라고 적혀 있었다. 안드로이드에서 iOS 문구가 나오고 있었다.
R8 이 지운 것
R8 은 무엇을 지웠는지 기록을 남긴다. mapping/release/usage.txt 다.
처음에는 열어보니 결제 콜백이 지워져 있었다. 상품 정보 응답 콜백, 상수 클래스 통째, BillingClient 빌더 메서드 몇 개. Cordova 플러그인은 문자열로 클래스를 찾고 인터페이스 콜백으로만 불리기 때문에 R8 의 참조 그래프에 안 잡힌다. keep 규칙을 넣었다. 그래도 실패했다.
다음에는 범위를 넓혀 봤다. 플러그인만이 아니었다. org.apache.cordova 전체가 잘려 있었고, JS 와 네이티브를 잇는 브리지 클래스가 통째로 없었다. 플러그인만 지키고 프레임워크는 안 지킨 것이다. 프레임워크까지 keep 에 넣으니 제거 항목이 98건에서 9건으로 줄었다. 이번에도 실패했다.
이 시점에 실패 지점마다 계측을 붙여 배포했고, 처음으로 값이 나왔다.
[IAP] store_init_timeout
[IAP] product_not_found registered: 0 ← 3ms 뒤3밀리초. 그리고 logcat 에 BillingClient 로그가 단 한 줄도 없었다. 네이티브 호출이 나가지도 않았다는 뜻이다.
그다음에는 앱 안을 직접 열었다. 릴리스 빌드에 웹뷰 디버깅을 켜고 DevTools 프로토콜로 붙어서, 앱 코드와 같은 경로로 초기화를 직접 실행했다.
code: 6777001
message: "Init failed - Class not found"
platform: android-playstore드디어 실제 에러 메시지가 나왔다. 그런데 apkanalyzer 로 APK 를 열어보니 클래스는 전부 있었다.
cc.fovea.PurchasePlugin 있음
org.apache.cordova.PluginManager 있음
org.apache.cordova.ExposedJsApi 있음
res/xml/config.xml 없음클래스는 다 있었고, 없는 건 플러그인 등록부인 res/xml/config.xml 하나였다.
원인은 두 개였다
config.xml 은 Cordova 플러그인 등록부다. 어떤 서비스 이름이 어떤 클래스에 대응하는지가 여기 적혀 있다. Capacitor 는 이 파일을 리소스 ID 로 리플렉션 조회하는데, 리소스 축소기는 그 조회를 못 본다. 참조가 없다고 판단해서 지웠다.
노드에 커서를 올리면 연결된 것만 남는다
| 원인 | 켠 시점 | 무엇이 사라졌나 |
|---|---|---|
| 리소스 축소 | 9/1 | 플러그인 등록부 res/xml/config.xml |
| 코드 축소 | 8/31 | 결제 콜백, Cordova 브리지 클래스 |
둘 다 증상이 똑같다. "처리 중…" 에서 멈춘다. 그래서 하나를 정확히 고쳐도 화면은 그대로였고, 나는 맞게 고쳐 놓고도 둘 다 헛수고라고 생각했다.
첫 수정이 논리적으로 맞았는데도 증상이 그대로였을 때, 가설을 버리기 전에 원인이 두 개일 가능성부터 의심해 봤으면 나았을 것 같다.
고친 것
리소스 축소기에게 등록부를 남기라고 알려주는 파일 한 줄.
<!-- android/app/src/main/res/raw/keep.xml -->
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@xml/config" />코드 축소는 네 줄.
-keep class cc.fovea.** { *; }
-keep class com.android.billingclient.** { *; }
-keep class org.apache.cordova.** { *; }
-keep class com.getcapacitor.cordova.** { *; }에러 분류는 목록을 하나로 줄였다.
// before: [6777001, 6777002, 6777006] ← SETUP, LOAD 까지 취소로 묶여 있었다
const userCancelCodes = [6777006];그리고 실패 지점 아홉 곳에 이벤트와 Sentry 메시지를 붙였다. 플러그인 부재, 초기화 타임아웃, 상품 없음, 검증 실패. 다음에 이 경로가 죽으면 매출을 보기 전에 알림으로 먼저 알게 된다.
같은 폰에서 다시 눌러보니 1.1초 만에 초기화가 끝나고 상품 네 개가 올라왔다. 9월 10일 23시 41분에 프로덕션으로 승격했다.
확인하는 법
같은 사고를 겪는 사람을 위해 검증 명령을 남긴다. 리소스 쪽이 특히 함정인데, APK 안에서 경로가 res/ZW.xml 처럼 축약되기 때문에 파일 이름으로 찾으면 못 찾는다.
# R8 이 지운 것 (빌드 후)
grep -c "cc.fovea" android/app/build/outputs/mapping/release/usage.txt
# 리소스가 살아있는지 — 파일 이름 말고 매핑을 봐야 한다
grep "xml:config" android/app/build/outputs/mapping/release/resources.txt
# 실기기에서 네이티브까지 갔는지
adb logcat | grep "D/CdvPurchase"확인을 건너뛴 릴리스
손실 자체는 작다. 8월 앱 결제 11건 69,300원이 9월에 0건이 된 것이다. 그보다 매출 경로 하나가 완전히 죽어 있었는데 열흘 동안 아무 장치도 그걸 알려주지 않았다는 게 문제였다.
결제를 시도하다 포기한 사람이 몇 명인지는 영영 모른다. 실패가 서버까지 오지 않았으니 셀 데이터가 없다.
나는 릴리스마다 하던 확인을 어느 순간부터 "지금까지 괜찮았으니까" 로 대체하고 있었고, 그 대체가 처음 실패한 날이 하필 빌드 설정을 바꾼 날이었다. 스토어 기한 압박도 있었다. 기한이 있는 릴리스일수록 내부 테스트 트랙을 거쳤어야 했는데, 그때 판단은 반대로 갔다.
Capacitor 자체 플러그인은 라이브러리가 keep 규칙을 같이 배포해서 보호되는데, Cordova 호환 플러그인은 그렇지 않다. 평소에는 웹만 배포하니 이 층이 있다는 것 자체를 잊고 지냈고, 네이티브 빌드를 건드린 이번에 그게 드러났다.
아직 하루가 안 지났다. 실기기에서 초기화와 상품 조회까지는 확인했지만, 실제 사용자의 결제가 다시 들어오는 걸 보기 전까지는 고쳐졌다고 말하기 어렵다. 며칠은 더 지켜봐야 할 것 같다.
끝