겪은 것과 고친 것 · 2026년 11월 6일
게임은 웹페이지다. 그것을 구글 플레이가 받는 .aab 로
바꾸는 일을 적는다. 하루를 꼬박 쓴 길이라, 다음 사람이 같은 자리에서
넘어지지 않도록 증상과 까닭을 함께 남긴다.
처음에는 TWA 로 쌌다. 크롬이 우리 웹사이트를 대신 띄워 주는 방식이다. 가볍고 쉬웠지만 두 가지가 걸렸다.
저장 자리가 크롬 안에서 앱 안으로 바뀐다. 옮기는 순간 그때까지 쌓인 기록이 사라진다는 뜻이다. 쓰는 사람이 0 명인 지금이 이 이사가 공짜인 유일한 때였다. 출시 뒤였다면 전원 초기화에 환불 요구였다.
게임은 Digital Goods API 로 적혀 있다. 그 API 는 TWA 안에서만 크롬이 내준다.
Capacitor 로 옮기면 없어진다. 그래서 게임을 고치는 대신,
없어진 API 를 흉내 내는 중계를 하나 둔다 — app/src/native.js.
window.getDigitalGoodsService(url) → svc
svc.getDetails(ids) → [{itemId, price}]
svc.listPurchases() → [{itemId, purchaseToken}]
svc.consume(token)
new PaymentRequest(...).show() → {details:{token}, complete()}
게임이 쓰는 것은 이 다섯뿐이다. 다섯을 그대로 돌려주면
index.html 은 한 글자도 안 고쳐도 되고, 웹판과 앱판이 한 벌의 코드로 간다.
C:\natmalsan 아래에 푼다.
폴더 이름은 src.C:\natmalsan\src\app\android.
끝의 app 이 아니라 android 다.app·capacitor-android·
capacitor-cordova-android-plugins 셋이 다 보이면 끝난 것이다.signing.keystore → release → Create.다른 키로 서명하면 플레이가 업데이트로 받아 주지 않는다. 키를 잃으면
이 앱은 영영 못 고친다. versionCode 는 올릴 때마다 커져야 한다.
C:\natmalsan\src 처럼 짧고 영문이고 OneDrive 밖인 자리로 옮긴다.The project is using an incompatible version (AGP 8.13.0)compileSdk 36 과 아슬아슬해지고 다음 업데이트 때 또 걸린다.No matching variant of project :capacitor-android … No variants existcapacitor.settings.gradle 이 :capacitor-android 를
../node_modules/@capacitor/android/capacitor 에서 찾는다. 그런데 그 폴더는
보통 git 에 안 올린다. ZIP 만 받은 사람에게는 그 자리가 비어 있다.assets/public, capacitor-cordova-android-plugins,
capacitor.config.json)도 함께 올려 두면, 받는 쪽에 Node 가 없어도
Android Studio 하나로 빌드된다.<head> 나
그 닫는 짝을 찾아 제 코드를 꽂는다. 못 찾으면 로그에 한 줄 남기고 조용히 포기한다 —
Unable to inject Capacitor, Plugins won't work. 우리 index.html
에는 html·head·body 가 하나도 없다. 브라우저는
알아서 보충하므로 웹에서는 삼 년을 써도 탈이 없었다.app/prep.py 가 www 로 옮긴 사본에만 넣는다 —
머리에 올 수 없는 첫 요소 바로 앞, 파서에게도 맞는 자리. 원본은 건드리지 않는다.native.js 도 그 뒤에 와야 한다 —
앞에 있으면 window.Capacitor 가 아직 없어 중계가 저를 웹인 줄 안다.cdv=0 — 결제 플러그인의 키가 0 개store.js 는 module.exports 에 아무것도 담지 않고
제 것을 window.CdvPurchase 에 직접 꽂는다. Cordova 는 그 자리를
module.exports — 빈 객체 — 로 덮어쓴다. 한 박자 뒤에 플러그인이 제 손으로
되살리지만 순서가 기기마다 다르다.try { obj[key] = value } ... if (obj[key] !== value) defineGetter(...).
받아 주지 않으면 돌려받을 수 없는 getter 로 바꿔 버린다.
막은 행동이 막을 수 없는 방법을 불러들였다.window.__nmKeep 에 적어 둔다. 그 뒤에 무엇으로 덮이든 적바림은 남는다.cdv=0 keep=0 — 적바림마저 비어 있다store.js 는 통째로 cordova.define(id, function(){...}) 한 덩어리고
본문은 require 될 때만 돈다. 그 일이 이 기기에서는 일어나지 않았다.cordova.define 을 잠깐 가로채 안에 든 함수만 빼내고 직접 부른다.
본문이 제 손으로 창에 꽂는다.android/app/src/main/assets/public 이고, www 에서 그리로
옮기는 것은 Gradle 이 아니라 cap copy 다. sync.sh 만 돌리면
www 는 새 판인데 assets 는 옛 판이다 — 그 둘이 어긋나도
아무도 성내지 않는다. 빌드는 성공하고, 스토어에도 올라가고, 폰에서만 조용히
아무것도 안 바뀐다.sync.sh 끝에
npx cap copy android 를 붙여 한 줄로 묶었다. 올리기 전에는
cmp www/index.html android/app/src/main/assets/public/index.html 로
한 번 맞춰 본다.me 로 세우는 몇 줄이
첫 길(cloudBoot) 안에만 있어서, 앱이 쓰는 두 번째 길
(exchangeCodeForSession) 에는 없었다. me 가 비면 화면도 모르고,
첫 줄에서 me 를 보는 cloudPush·cloudSync 도 그대로
돌아 나간다 — 로그인은 됐는데 아무것도 안 되는 꼴이다.adoptUser() 로 꺼내 두 길이 같은 자리를 쓰게 했다.
길이 둘이면 손질하는 자리도 하나여야 한다 — 여덟째와 같은 덫이다.폰 안에서 무슨 일이 벌어지는지 밖에서는 볼 수 없다. 그래서 앱을 열고 넉 초 뒤 화면 아래에 한 줄을 적게 했다. 이 한 줄이 하루를 끝냈다.
v9 cap=Y nat=Y cv=Y cdv=33 keep=33 st=Y dgs=Y gd=Y rq=0 h=localhost
| 글자 | 뜻 | 어긋나면 |
|---|---|---|
| cap | window.Capacitor 가 있나 | N 이면 함정 넷 |
| nat | 네이티브로 보나 | N 이면 웹으로 연 것 |
| cv | cordova 가 있나 | N 이면 플러그인 미포함 |
| cdv | CdvPurchase 의 키 수 | 0 이면 함정 다섯·여섯 |
| keep | 우리 적바림의 키 수 | 0 이면 본문이 안 돌았다 |
| st | store 객체가 있나 | Y 라야 값이 뜬다 |
| dgs | 중계가 붙었나 | N 이면 native.js 가 터졌다 |
| ERR | 처음 터진 오류 | 적혀 있으면 그게 범인 |
결제가 자리를 잡으면 이 줄은 지운다. 길을 잃지 않기 위한 등불이지 사용자에게 보일 것은 아니다.
assets/public 의 cordova.js·
cordova_plugins.js·store.js 를 앱과 같은 순서로 이어 붙이고
가짜 window.Capacitor 를 얹으면, 폰에서 나는 일이 대부분 그대로 난다.로그인이 크롬을 연다. 게임이 구글·카카오 로그인 주소로 이동하면 Capacitor 가
제 주소 밖이라고 보고 바깥 브라우저로 넘긴다. 설계대로 도는 것이지 버그가 아니다.
고치려면 Capacitor 쪽에 그 주소들을 허용하고, Supabase 쪽 돌아올 주소에
https://localhost 를 더해야 한다. 결제가 먼저라 미뤄 두었다.
광고는 1.1 에서. AdMob 은 앱이 스토어에 올라간 뒤에야 심사해 준다.
자리는 native.js 의 NativeAds.rewarded() 에 비워 두었다.