Expo 로컬 릴리스 빌드: 17분 만에 죽고 3분 48초에 끝난 이유
클라우드 빌드를 떠나 로컬에서 릴리스를 만들어 보기로 했습니다. 서명 자료는 다 모아 두었고 도구도 깔았습니다. 첫 빌드는 17분 51초를 돌고 나서 죽었습니다.
Execution failed for task ':expo-updates:kspReleaseKotlin'.
> A failure occurred while executing com.google.devtools.ksp.gradle.KspAAWorkerAction
> Metaspace649개 태스크가 실행된 뒤였습니다. 거의 끝난 자리에서 메모리가 모자랐습니다.
릴리스를 못 견디는 기본 메모리 설정
android/gradle.properties 를 열어 보니 이렇게 적혀 있었습니다.
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512mExpo 템플릿의 기본값입니다. 개발 빌드는 이 안에서 끝나는데, 릴리스에서 Kotlin Symbol Processing 이 도는 순간 Metaspace 512MB 를 넘깁니다. 힙이 아니라 메타스페이스라는 점이 헷갈리는 대목입니다. -Xmx 만 올려서는 같은 자리에서 다시 죽습니다.
16GB 기계에서 이렇게 올렸습니다.
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=2048m고치면 안 되는 그 파일
여기서 한 번 멈춰야 합니다. android/ 는 npx expo prebuild 가 만드는 산출물이고 .gitignore 로 막혀 있습니다. gradle.properties 도 그 안에 있습니다. 손으로 고쳐 놓으면 다음에 prebuild 를 돌리는 순간 기본값으로 돌아갑니다.
그러면 다음에 릴리스를 빌드하는 사람이 17분을 다시 씁니다. 그 사람이 몇 달 뒤의 자신일 가능성이 높습니다.
그래서 config plugin 에 넣었습니다. 플러그인은 생성 과정에 끼어들어 매번 같은 수정을 다시 적용합니다.
withGradleProperties(config, (cfg) => {
const KEY = 'org.gradle.jvmargs';
const VALUE = '-Xmx4096m -XX:MaxMetaspaceSize=2048m';
const found = cfg.modResults.find((it) => it.type === 'property' && it.key === KEY);
if (found) found.value = VALUE;
else cfg.modResults.push({ type: 'property', key: KEY, value: VALUE });
return cfg;
});서명까지 들고 있는 같은 플러그인
이 플러그인은 원래 다른 일을 하려고 만든 것이었습니다. Expo 가 만드는 build.gradle 은 릴리스 빌드가 디버그 키를 가리킵니다.
release {
// Caution! In production, you need to generate your own keystore file.
signingConfig signingConfigs.debug
}그 디버그 키스토어는 비밀번호가 android 이고 별칭이 androiddebugkey 인, 알라고 만든 값입니다. 그대로 빌드하면 서명은 되고 AAB 도 나옵니다. 거부는 Play 에 올릴 때 옵니다.
그래서 플러그인이 signingConfigs.release 를 만들고 buildTypes.release 가 그것을 가리키게 바꿉니다. 키스토어 경로와 비밀번호는 파일에 적지 않고 빌드할 때 환경변수로만 넣습니다.
release {
storeFile file(System.getenv("DUOLABS_KEYSTORE") ?: "...")
storePassword System.getenv("DUOLABS_STORE_PASSWORD")
keyAlias System.getenv("DUOLABS_KEY_ALIAS")
keyPassword System.getenv("DUOLABS_KEY_PASSWORD")
}메모리 설정도 서명 설정도 같은 이유로 같은 자리에 있습니다. 생성물을 고치지 않고 생성 과정을 고친다는 것 하나입니다.
3분 48초로 끝난 두 번째 빌드
prebuild 를 다시 돌려 두 설정이 모두 살아 있는 것을 확인하고 빌드했습니다.
BUILD SUCCESSFUL in 3m 48s
769 actionable tasks: 737 executed, 32 up-to-date17분과 4분의 차이는 대부분 캐시입니다. 죽은 빌드도 649개 태스크의 결과는 남겨 두었습니다. 실패한 빌드가 아무것도 남기지 않는다고 생각하면 재시도가 더 무섭게 느껴집니다.
이름이 아니라 지문으로 하는 서명 확인
빌드가 성공했다는 것과 올바른 키로 서명됐다는 것은 다릅니다. 디버그 키로 서명해도 빌드는 성공합니다. 그래서 결과물의 인증서 지문을 키스토어의 것과 맞춰 봤습니다.
$ keytool -printcert -jarfile app-release.aab
SHA1: E5:73:41:91:8D:...
$ keytool -list -v -keystore upload.jks
SHA1: E5:73:41:91:8D:...같았습니다. 소유자 필드가 CN=, OU=, O= 처럼 비어 있는데, 자동 생성된 업로드 키스토어에서는 흔한 모양입니다. 이름으로 확인했다면 여기서 헷갈렸을 것입니다.
결과
같은 날 iOS 도 같은 길로 갔습니다. 아카이브를 만들고 IPA 로 뽑아 App Store Connect 에 올렸습니다. 빌드 서비스도, Xcode 계정 로그인도 쓰지 않았습니다. 안드로이드는 서명된 AAB 까지 만들고 업로드는 다음으로 미뤘습니다.
떠나 보니 클라우드 빌드가 해 주던 일은 서명만이 아니었습니다. 메모리 설정처럼 눈에 띄지도 않는 것들이 함께 딸려 있었습니다. 그 목록은 먼저 적어 둔 글에 있습니다.
함께 읽기
- Expo config plugin이 반영되지 않는 이유: prebuild와 mod 순서생명시계 앱에 Face ID 잠금을 붙였습니다. expo-local-authentication 을 설치하고 app.json 에 플러그인과 권한 문구를 적었습니다.
- Android Studio 없이 실기기 빌드: 최소 환경 838MB와 각 구성요소의 역할Expo SDK 57 앱을 안드로이드 실기기에 올리려고 맥을 열었는데, 빌드에 필요한 것이 하나도 없었습니다. java -version은 "Unable to locate a Java Runtime"을 냈고 ANDROIDHOME은 비어 있었으며 adb는 명령어 자체가 없었습니다. Xcode만 들어 있는 기계였습니다.
- Expo CNG: android 와 ios 폴더가 리포에 없는 이유Expo 로 만든 앱의 리포를 클론하면 ios/ 도 android/ 도 없습니다. .gitignore 를 열어 보면 아예 막혀 있습니다.
- EAS 없이 Expo 로컬 빌드: 서명 자료를 모으고도 남는 다섯 가지클라우드 빌드를 떠나기로 하고(왜 옮기는지는 따로 적었습니다) 서명 자료부터 모았습니다. iOS 배포 인증서와 프로비저닝 프로파일, 안드로이드 업로드 키스토어와 비밀번호 두 개, 스토어 API 키까지 한자리에 놓고 확인했습니다. 코드 서명 신원도 유효하게 잡혔습니다.
- Play 스토어 등록정보 자동화: EAS가 덮지 않는 한 칸앱에 언어 셋을 더하고 스토어에 올리려던 참이었습니다. iOS 쪽은 명령 하나로 끝났습니다.