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 까지 만들고 업로드는 다음으로 미뤘습니다.
떠나 보니 클라우드 빌드가 해 주던 일은 서명만이 아니었습니다. 메모리 설정처럼 눈에 띄지도 않는 것들이 함께 딸려 있었습니다. 그 목록은 먼저 적어 둔 글에 있습니다.
함께 읽기
- EAS 없이 Expo 로컬 빌드: 서명 자료를 모으고도 남는 다섯 가지클라우드 빌드를 떠나기로 하고(왜 옮기는지는 따로 적었습니다) 서명 자료부터 모았습니다. iOS 배포 인증서와 프로비저닝 프로파일, 안드로이드 업로드 키스토어와 비밀번호 두 개, 스토어 API 키까지 한자리에 놓고 확인했습니다. 코드 서명 신원도 유효하게 잡혔습니다.
- EAS 빌드 큐 40분, 로컬 5분: 로컬로 옮기기 전에 백업해야 할 것앱 하나를 출시하려고 EAS에 안드로이드 production 빌드를 걸었습니다. 큐에서 40분을 기다렸습니다. 무료 티어라 빌드 머신이 빌 때까지 순서를 기다리는 구조입니다.
- Expo SDK 56 iOS 빌드 오류와 Swift weak let 호환성저는 견적킷을 iPhone에서 실행하려다 xcodebuild 코드 65를 만났습니다. 빌드 화면에는 Reanimated와 Worklets 컴파일 로그가 길게 지나갔고, 마지막에는 오류 15개가 있다는 메시지만 남았습니다.
- Expo에서 app.json 수정이 OTA 업데이트에 반영되지 않는 이유Expo 앱의 app.json을 고친 뒤 저장소에 푸시했습니다. production 채널의 OTA 작업도 평소처럼 실행됐습니다. 배포 작업이 성공하면 변경 사항이 앱에 들어간 것처럼 느껴집니다. 이번에는 그렇지 않았습니다.
- Expo 앱 스토어 배포 준비 체크리스트: 순서가 중요한 이유새 Expo 앱 하나를 두 스토어에 올릴 준비를 했습니다. 필요한 항목 자체는 검색하면 다 나옵니다. 그런데 정작 저를 막은 건 항목이 아니라 순서였습니다. 앞 단계가 끝나야 뒤 단계의 값이 생기는 구간이 몇 군데 있고, 그걸 모르고 덤비면 "왜 이 필드가 비어 있지"에서 멈춥니다.