앱으로 싸기

겪은 것과 고친 것 · 2026년 11월 6일

게임은 웹페이지다. 그것을 구글 플레이가 받는 .aab 로 바꾸는 일을 적는다. 하루를 꼬박 쓴 길이라, 다음 사람이 같은 자리에서 넘어지지 않도록 증상과 까닭을 함께 남긴다.

왜 TWA 를 버리고 Capacitor 로 왔나

처음에는 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 은 한 글자도 안 고쳐도 되고, 웹판과 앱판이 한 벌의 코드로 간다.

AAB 만드는 길

  1. 저장소를 ZIP 으로 받아 C:\natmalsan 아래에 푼다. 폴더 이름은 src.
  2. Android Studio → Open → C:\natmalsan\src\app\android. 끝의 app 이 아니라 android 다.
  3. JVM 을 물으면 Use JVM 21. 25 는 안 된다.
  4. Gradle sync 를 기다린다. 좌측에 app·capacitor-android· capacitor-cordova-android-plugins 셋이 다 보이면 끝난 것이다.
  5. Build → Generate Signed App Bundle → Android App Bundle → 기존 signing.keystore → release → Create.
  6. Play Console → 내부 테스트 → 새 버전 → 올리고 「저장 및 출시」까지 누른다.
서명키는 반드시 TWA 때 쓰던 그것

다른 키로 서명하면 플레이가 업데이트로 받아 주지 않는다. 키를 잃으면 이 앱은 영영 못 고친다. versionCode 는 올릴 때마다 커져야 한다.

넘어진 자리 아홉

하나
폴더 경로에 한글·OneDrive·괄호
증상
까닭 모를 빌드 실패
까닭
OneDrive 는 파일을 온라인 전용으로 바꾸거나 동기화 중 잠근다. 한글·공백·괄호가 든 경로에서 Gradle 과 그 아래 도구들이 깨진다.
고침
C:\natmalsan\src 처럼 짧고 영문이고 OneDrive 밖인 자리로 옮긴다.
둘
Android Studio 가 낡았다
증상
The project is using an incompatible version (AGP 8.13.0)
까닭
빌드 도구가 편집기보다 새것이면 편집기가 열기를 거부한다.
고침
Android Studio 를 최신으로 올린다. AGP 를 낮추지 않는다 — 낮추면 compileSdk 36 과 아슬아슬해지고 다음 업데이트 때 또 걸린다.
셋
node_modules 가 ZIP 에 없다
증상
No matching variant of project :capacitor-android … No variants exist
까닭
capacitor.settings.gradle 이 :capacitor-android 를 ../node_modules/@capacitor/android/capacitor 에서 찾는다. 그런데 그 폴더는 보통 git 에 안 올린다. ZIP 만 받은 사람에게는 그 자리가 비어 있다.
고침
688KB 뿐이다. 그것 하나만 예외로 커밋한다. 생성물 (assets/public, capacitor-cordova-android-plugins, capacitor.config.json)도 함께 올려 두면, 받는 쪽에 Node 가 없어도 Android Studio 하나로 빌드된다.
넷
index.html 에 닫는 머리표가 없다
증상
앱은 열리는데 장터에 값이 안 뜨고 결제가 안 붙는다
까닭
Capacitor 는 HTML 에서 글자 그대로 <head> 나 그 닫는 짝을 찾아 제 코드를 꽂는다. 못 찾으면 로그에 한 줄 남기고 조용히 포기한다 — Unable to inject Capacitor, Plugins won't work. 우리 index.html 에는 html·head·body 가 하나도 없다. 브라우저는 알아서 보충하므로 웹에서는 삼 년을 써도 탈이 없었다.
고침
app/prep.py 가 www 로 옮긴 사본에만 넣는다 — 머리에 올 수 없는 첫 요소 바로 앞, 파서에게도 맞는 자리. 원본은 건드리지 않는다.
덧
주석 안에 그 글자를 적으면 안 된다. Capacitor 는 먼저 만나는 쪽에 꽂으므로 주석이 제 코드를 엉뚱한 데로 끌어간다. native.js 도 그 뒤에 와야 한다 — 앞에 있으면 window.Capacitor 가 아직 없어 중계가 저를 웹인 줄 안다.
다섯
막으면 더 세게 때려 박힌다
증상
진단판에 cdv=0 — 결제 플러그인의 키가 0 개
까닭
store.js 는 module.exports 에 아무것도 담지 않고 제 것을 window.CdvPurchase 에 직접 꽂는다. Cordova 는 그 자리를 module.exports — 빈 객체 — 로 덮어쓴다. 한 박자 뒤에 플러그인이 제 손으로 되살리지만 순서가 기기마다 다르다.

빈 것을 물리치는 자리 지킴을 두었더니 더 나빠졌다. Cordova 의 코드는 이렇다 — try { obj[key] = value } ... if (obj[key] !== value) defineGetter(...). 받아 주지 않으면 돌려받을 수 없는 getter 로 바꿔 버린다. 막은 행동이 막을 수 없는 방법을 불러들였다.
고침
막지 않는다. 받는 척 다 받아 주고, 알맹이가 든 것만 조용히 window.__nmKeep 에 적어 둔다. 그 뒤에 무엇으로 덮이든 적바림은 남는다.
여섯
플러그인 본문이 아예 안 돈다
증상
진단판에 cdv=0 keep=0 — 적바림마저 비어 있다
까닭
덮어쓰기에 당한 것이 아니라, 플러그인이 제 알맹이를 만든 적이 없다는 뜻이다. store.js 는 통째로 cordova.define(id, function(){...}) 한 덩어리고 본문은 require 될 때만 돈다. 그 일이 이 기기에서는 일어나지 않았다.
고침
기다리지 않는다. 3 초를 세어 보고도 없으면 파일을 다시 받아, cordova.define 을 잠깐 가로채 안에 든 함수만 빼내고 직접 부른다. 본문이 제 손으로 창에 꽂는다.
일곱
적응형 아이콘은 바깥이 깎인다
증상
삼성 폰에서 해가 잘렸다
까닭
108dp 판 가운데 66dp 안에 든 것만 반드시 보인다. 바깥은 폰마다 다르게 깎인다.
고침
해 꼭대기가 그림 높이의 8% 지점이므로 70% 로 줄이면 안전 구역 안에 머문다 (0.42 × 0.70 = 0.294 < 0.305). 바탕색은 눈대중으로 고르지 말고 아이콘 제 가장자리를 재서 쓴다 — 한 눈금 어긋나면 그 한 눈금이 네모 자국으로 보인다.
여덟
www/ 는 앱이 읽는 자리가 아니다
증상
고친 것이 분명한데 앱은 옛 판 그대로다. 빌드도 업로드도 성공한다.
까닭
진짜로 앱에 담기는 것은 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
글자뜻어긋나면
capwindow.Capacitor 가 있나N 이면 함정 넷
nat네이티브로 보나N 이면 웹으로 연 것
cvcordova 가 있나N 이면 플러그인 미포함
cdvCdvPurchase 의 키 수0 이면 함정 다섯·여섯
keep우리 적바림의 키 수0 이면 본문이 안 돌았다
ststore 객체가 있나Y 라야 값이 뜬다
dgs중계가 붙었나N 이면 native.js 가 터졌다
ERR처음 터진 오류적혀 있으면 그게 범인

결제가 자리를 잡으면 이 줄은 지운다. 길을 잃지 않기 위한 등불이지 사용자에게 보일 것은 아니다.

다음에 또 막히면

아직 남은 것

로그인이 크롬을 연다. 게임이 구글·카카오 로그인 주소로 이동하면 Capacitor 가 제 주소 밖이라고 보고 바깥 브라우저로 넘긴다. 설계대로 도는 것이지 버그가 아니다. 고치려면 Capacitor 쪽에 그 주소들을 허용하고, Supabase 쪽 돌아올 주소에 https://localhost 를 더해야 한다. 결제가 먼저라 미뤄 두었다.

광고는 1.1 에서. AdMob 은 앱이 스토어에 올라간 뒤에야 심사해 준다. 자리는 native.js 의 NativeAds.rewarded() 에 비워 두었다.

문서 목록으로 · 게임 열기