웹앱을 자동 업데이트 가능한 Android APK로 만들기
앱 설정으로 재사용하는 WebView 보일러플레이트와 Codex 스킬. APK 자동 업데이트, 서명키 생성·백업·복원, 빌드·게시 검증을 정리한다.
글 관리 · 공개 범위웹앱을 Android 앱으로 반복해서 배포할 때 공통으로 남는 작업은 웹 화면을 여는 코드보다 앱의 신원을 유지하며 새 APK를 전달하는 과정이다. 앱 이름과 주소는 설정으로 분리하고, 서명키·버전·파일 검증·설치 확인을 공통 구현으로 묶으면 다음 앱에서도 같은 배포 절차를 재사용할 수 있다.
android-webapp-apk는 이 과정을 담은 WebView 보일러플레이트와 Codex 스킬이다. 앱 설정 하나로 독립 프로젝트를 생성하며, 생성된 앱 안에 Android 소스·업데이트 서버·서명 및 배포 도구가 함께 들어간다. 여기서는 2026-10-13 기준 코드를 기준으로 구조와 사용법을 설명한다.
웹 업데이트와 APK 업데이트를 나누기
웹앱의 화면·업무 로직을 웹 서버에 배포하면 앱의 WebView도 해당 서버의 콘텐츠를 읽는다. 반면 WebView 설정, Android 권한, 네이티브 코드처럼 APK에 들어 있는 내용은 새 APK가 필요하다. 서비스워커나 HTTP 캐시를 쓰는 웹앱은 웹 배포 자체의 캐시 갱신 절차도 따로 관리한다.
| 바뀌는 내용 | 배포 대상 | 이 보일러플레이트의 역할 |
|---|---|---|
| 웹 화면, 서버 API | 웹 서버 | 설정된 HTTPS 주소를 연다 |
| 네이티브 기능, WebView 정책 | 서명한 APK | 새 버전 확인·다운로드·파일 검증·설치 확인 |
| 앱 이름, 시작 주소, 패키지명 | 앱별 설정 | 생성할 프로젝트의 설정을 구성한다 |
| 서명키, 비밀번호 | 비공개 외부 저장소 | 빌드 때 읽으며 Git과 공개 서버에는 넣지 않는다 |
이 구현의 자동 업데이트는 앱 실행·복귀 시 확인 → 자동 다운로드 → 검증 → Android 설치 확인이다. 확인에는 15분 중복 제한이 있고, 설치를 미루면 24시간 유예한다. 앱이 종료된 동안 계속 확인하는 작업은 포함하지 않는다. API 31 이상에서는 USER_ACTION_REQUIRED를 명시한다. 이는 이 템플릿이 선택한 설치 정책이며, Android가 제공하는 모든 업데이트 방식을 뜻하지는 않는다. PackageInstaller 공식 문서
앱 설정 하나로 프로젝트 만들기
공개 저장소를 복제한 뒤 examples/notes.json을 자신의 설정으로 복사한다. 예시 URL은 실제 서비스 주소로 바꿔야 한다.
{
"appName": "My Notes",
"applicationId": "com.example.notes",
"versionCode": 1,
"versionName": "0.1.0",
"homeUrl": "https://notes.example.com/app/home",
"internalUrls": ["https://notes.example.com/app"],
"updateBaseUrl": "https://updates.example.com/notes/",
"signingAlias": "notes-release"
}
applicationId는 설치된 앱의 식별자다. 기존 앱을 업데이트하려면 원래 값을 유지한다. versionCode는 업데이트 순서를 판정할 증가 정수이고, versionName은 표시와 배포 파일 이름에 사용하는 버전이다. 이 템플릿은 0.1.0 같은 숫자 세 부분 형식을 사용한다.
internalUrls는 WebView 안에 남길 HTTPS origin과 경로 접두사다. /app을 허용했다고 /application까지 허용하지 않는다. 외부 링크는 사용자의 클릭을 통해 외부 브라우저로 연결한다. 로그인에 다른 origin이 필요하다면 실제 인증 흐름을 확인해 필요한 경로만 추가한다. WebView와 일반 브라우저는 애플리케이션 데이터를 공유하는 동일 저장소가 아니므로, 웹에서 이미 로그인했다는 사실만으로 앱 로그인까지 확인됐다고 판단하지 않는다. WebView 공식 안내
저장소 루트에서 생성한다.
python3 skills/readiz-webapp-android/scripts/create_app.py \
--config /path/to/my-app.json \
--out /path/to/new-app
기존 대상 폴더는 덮어쓰지 않는다. 아이콘은 기본 벡터 대신 --icon /path/to/square.png로 지정할 수 있다. PNG는 정사각형 48–1024픽셀, 최대 1 MiB를 받는다. 생성된 프로젝트에는 다음 구성이 들어간다.
new-app/
app.json
android/ WebView, 업데이트 상태 처리, Android 검사
server/ 버전 메타데이터와 APK 제공
tools/ 서명키 생성, 빌드, 게시, 운영 대조
docs/ 서명키 생성·백업·복원 안내
템플릿의 Java 패키지는 공통 코드용 namespace로 유지하고 설치 식별자는 app.json에서 가져온다. 생성 결과는 원래 보일러플레이트 저장소 없이도 빌드할 수 있다. 이후 공통 템플릿이 바뀌더라도 기존 앱 소스에 자동으로 덮어쓰지 않으며, 필요한 변경을 각 앱에서 검토해 적용한다.
서명키는 새 앱의 첫 배포 때 만든다
APK의 해시는 파일이 같은지를 보여주고, 서명 인증서는 누가 그 APK를 서명했는지 확인하는 기준이 된다. 이 템플릿은 기존 앱과 동일한 서명 인증서를 요구한다. 키 교체와 signing lineage는 별도 구현 대상이다. Play App Signing의 업로드 키와 앱 서명키도 구분해야 한다. 직접 APK를 배포하는 이 구성에서는 보유한 앱 서명키를 계속 관리한다. Android 앱 서명 문서
먼저 JDK 17, Android SDK platform 36와 build-tools 35.0.0, Gradle 8.13을 준비한다. 다음 경로는 실제 설치 위치로 바꾼다.
export JAVA_HOME=/path/to/jdk-17
export ANDROID_HOME=/path/to/android-sdk
export GRADLE_HOME=/path/to/gradle-8.13
생성된 앱의 루트에서 새 앱 최초 한 번만 실행한다.
python3 tools/init_signing.py --new-app
기본 키 디렉터리는 사용자의 설정 폴더 아래 android-webapp-apk/<applicationId>/signing/이다. 다른 위치를 쓰려면 생성과 빌드에 같은 WEBAPP_SIGNING_DIR을 설정한다. 앱별로 키를 분리하면 다른 앱의 키를 잘못 연결한 사고도 구분하기 쉽다.
이 명령은 RSA 3072비트 키와 10,000일 유효기간 인증서를 생성한다. 키스토어 포맷은 PKCS12이며 파일 이름은 도구 규약상 release.jks다. alias는 app.json의 signingAlias에서 가져온다. 무작위 비밀번호는 password 파일에 저장하고 공개 인증서는 certificate.pem으로 내보낸다. 키 디렉터리는 소유자 전용, 키와 비밀번호는 소유자 읽기·쓰기 권한으로 생성한다.
기존 앱에서는 이 명령으로 새 키를 만들지 않는다. 원래 키와 비밀번호를 복원하거나 원래 디렉터리를 연결한다. 빌드 도구는 키가 없으면 중단하며 자동 생성하지 않는다. 비밀번호나 alias가 같아도 새로 만든 개인키는 원래 키가 아니다.
보관과 복원 때 확인할 것
키스토어와 비밀번호는 둘 다 복원할 수 있어야 한다. 저장소에는 비밀이 아닌 applicationId·alias·인증서 지문·버전 기록만 남기고, 키와 비밀번호는 Git 및 공개 릴리스에 넣지 않는다. 백업은 암호화하며, 백업 암호도 별도 복원 수단을 마련한다. 네트워크 저장소를 사용할 때는 실제 마운트와 쓰기 권한을 확인하고, 같은 저장 장치의 사본과 독립 백업을 구별한다.
복원은 원래 파일을 덮어쓰기 전에 별도 임시 디렉터리에서 수행한다. 이전 배포 APK의 인증서 SHA-256과 복원한 키의 인증서를 대조한다. 다음 명령은 비밀번호 원문을 명령행에 넣지 않고 파일에서 읽는다.
"$JAVA_HOME/bin/keytool" -list -v \
-keystore "$WEBAPP_SIGNING_DIR/release.jks" \
-storepass:file "$WEBAPP_SIGNING_DIR/password" \
-alias notes-release
"$ANDROID_HOME/build-tools/35.0.0/apksigner" \
verify --print-certs /path/to/previous.apk
템플릿의 새 키는 store password와 key password가 같은 단일 signer 구성이다. 기존 키가 다른 구성이면 키 비밀번호를 임의로 바꾸지 말고 빌드 설정을 맞춘다. 자세한 생성·암호화 백업·복원 절차는 서명키 관리 문서에 있다.
빌드와 게시를 별도 명령으로 두기
빌드 결과와 실제 서비스에서 제공하는 파일은 서로 다른 상태다. 생성 앱의 루트에서 먼저 새 출력 디렉터리로 빌드한다.
python3 tools/build.py --output /outside/project/build-0.1.0
이 명령은 Android 검사, release lint, 서명 빌드와 APK 패키지·버전 검증을 수행한다. 결과는 app-0.1.0.apk, 그 메타데이터, 인증서 지문을 담은 verification.json이다. 키와 산출물은 프로젝트 밖에 둔다.
검증한 APK를 게시할 때 별도 명령을 실행한다.
python3 tools/publish.py \
--apk /outside/project/build-0.1.0/app-0.1.0.apk \
--releases /outside/project/releases
게시기는 실제 APK를 다시 검사하고 기존 게시본의 인증서·패키지와 비교한다. 버전이 내려가거나 같은 versionName에 다른 APK 바이트를 덮어쓰는 요청은 거절한다. 버전 고정 APK와 메타데이터를 먼저 준비하고, 마지막에 최신 메타데이터를 원자적으로 바꾼다.
이 순서는 다운로드 중인 사용자를 보호한다. 앱이 0.1.0 메타데이터를 읽은 직후 서버가 0.1.1로 바뀌더라도, 0.1.0의 버전·해시로 지정한 파일을 계속 받을 수 있어야 한다. 서버 최신 포인터를 이전 버전으로 되돌리는 일과 이미 설치된 앱을 낮은 versionCode로 되돌리는 일도 다르다. 기기에 수정본을 배포할 때는 수정한 코드에 더 높은 versionCode를 부여한다.
업데이트 서버와 앱이 나눠 검증하기
서버는 updateBaseUrl 아래 두 경로만 제공한다.
| 경로 | 용도 |
|---|---|
app-version | packageName, versionCode, versionName, size, sha256 반환 |
app.apk | 현재 검증된 APK 반환 |
app.apk?version=0.1.0&sha256=… | 메타데이터에서 지정한 버전의 정확한 APK 반환 |
독립 Node 서버는 기본적으로 loopback 주소에서 실행한다. 운영 HTTPS 프록시와 경로 연결은 서비스에 맞게 구성한다.
node server/server.mjs \
--releases /outside/project/releases --port 8080
키 디렉터리나 전체 프로젝트를 정적 파일 루트로 공개할 필요가 없다. 서버는 APK의 실제 크기와 해시를 대조하고, 메타데이터에는 다섯 필드만 내보낸다. 기본 상한은 메타데이터 4 KiB, APK 16 MiB다. 더 큰 네이티브 기능을 추가한다면 실제 산출물 크기에 맞춰 클라이언트와 서버의 상한을 함께 검토한다.
앱은 전용 캐시에 임시 다운로드한 뒤 크기·SHA-256·패키지·정확한 버전·기존 서명 인증서를 확인한다. WebView 로그인 쿠키를 업데이트 요청에 복사하지 않으며, 임의의 리다이렉트도 따라가지 않는다. 설치 허용 설정에서 복귀하거나 설치를 취소했을 때도 같은 화면이 즉시 반복되지 않게 상태를 관리한다. 기본 템플릿에는 JavaScript bridge를 넣지 않았다. 웹에서 네이티브 권한을 호출하는 기능이 필요하면 해당 경계부터 별도로 설계한다.
운영 배포 뒤에는 공개 응답도 확인한다.
python3 tools/verify_deployment.py \
--manifest /outside/project/build-0.1.0/app-0.1.0.apk.json
버전 메타데이터, 최신 APK, 버전 고정 APK의 바이트와 해시를 로컬 검증본과 대조한다. 이 검사는 실계정 로그인을 하거나 기기에 설치하지 않는다.
스킬을 재사용하는 방법
저장소 루트에서 다음 명령으로 로컬 Codex의 스킬 경로에 연결한다.
python3 tools/install-skill.py
readiz-webapp-android 스킬은 생성기와 템플릿을 함께 가진다. 다른 환경으로 복사할 때도 SKILL.md만 가져오지 않고 scripts/, assets/, references/를 포함한 폴더 전체를 복사한다. 기존에 다른 스킬이 설치된 경로는 덮어쓰지 않는다.
다음처럼 작업을 요청할 수 있다.
$readiz-webapp-android를 사용해서 이 HTTPS 웹앱을 Android 앱으로 만들어줘. 기존 앱이면 패키지와 서명키, 웹 저장소 주소를 유지하고, 빌드와 업데이트 경로를 검증해줘.
스킬은 새 앱 생성과 기존 앱 갱신을 구분한다. 기존 WebView 앱을 TWA로 바꾸거나, 위젯·미디어 재생·TV 리모컨 기능을 한꺼번에 공통 템플릿에 넣지 않는다. 공통 배포 경로를 재사용하면서 앱 고유 기능은 해당 앱에서 확장하는 편이 변경 범위를 확인하기 쉽다.
검증한 범위
2026-10-13 기준 서로 다른 패키지명·URL·서명 alias를 가진 예시 앱 두 개에서 서명 APK 빌드와 Android 검사 28개가 통과했다. 생성기·키 보존·실패 정리를 다룬 Python 검사 6개, HTTP 제공 경계를 다룬 Node 검사 4개도 통과했다.
실제 서명 APK로 같은 키의 버전 상승, 다른 패키지 차단, 같은 패키지를 다른 키로 다시 서명한 APK 차단, 버전 하향 거절, 이전 버전 다운로드 유지를 확인했다. 이 결과는 공통 코드와 파일 전달 경로의 검증이며, 각 서비스의 실제 로그인과 물리 Android 기기 설치는 별도로 확인해야 한다. 재현 명령과 기록은 TESTING.md에 있다.
AI로 읽기 · Markdown
로그인 없이 읽는 Markdown 원문.
curl -fsSL 'https://www.readiz.com/llm-wiki/android-webapp-apk-auto-update-signing-skill/index.md'