Expo Managed Workflow에서 Android 서명 설정을 자동화하는 방법: system builder 프로젝트 기록
Expo로 앱을 개발하다 보면 반드시 마주치는 벽이 있습니다. 바로 구글 로그인과 같은 네이티브 기능 설정입니다. 특히 managed workflow에서는 프리빌드 과정에서 네이티브 폴더가 매번 초기화되기 때문에 수동으로 설정을 변경하는 것은 유지보수 측면에서 매우 위험합니다.
현재 제가 구축 중인 멀티 SaaS 자동화 시스템인 system builder에서는 이 문제를 Config Plugin을 통한 코드 주입 방식으로 해결했습니다. 그 긴 여정과 기술적인 해결책을 공유합니다.
고통의 시작: 구글 로그인 실패와 프리빌드
처음에는 단순히 구글 클라우드 콘솔에 SHA-1 지문을 등록하는 것으로 충분할 줄 알았습니다. 하지만 빌드 유형에 따라 서명이 달라지고, 특히 릴리즈 빌드에서 인증서가 제대로 먹히지 않아 구글 로그인 에러를 마주해야 했습니다.
이 문제를 해결하려면 android/app/build.gradle 파일을 직접 수정해야 하는데, Expo의 특성상 npx expo prebuild를 실행하면 공들여 수정해둔 코드가 모두 사라집니다. 결국 로컬 빌드와 EAS 클라우드 빌드 모두에서 일관된 서명 설정을 유지하려면 인프라 자체를 코드화해야 한다는 결론에 도달했습니다.
해결책: Custom Config Plugin 제작
Expo Config Plugin은 빌드 시점에 네이티브 프로젝트 코드를 수정해주는 일종의 로봇입니다. 저는 withAppBuildGradle을 활용해 Gradle 파일에 직접 서명 로직을 주입하는 플러그인을 만들었습니다.
이 플러그인의 핵심 기능은 다음과 같습니다.
- 프로젝트 루트의 외부 파일(keystore.properties)을 읽어오는 로직 주입
- signingConfigs 설정을 동적으로 교체하여 보안 유지
- 릴리즈 빌드 시에도 정확한 서명 객체를 참조하도록 강제 연결
실제 구현 코드
아래는 system builder 프로젝트에서 사용 중인 플러그인 소스 코드입니다.
JavaScript
const { withAppBuildGradle } = require("@expo/config-plugins");
/**
* Android build.gradle에 keystore.properties 로드 로직과
* 서명 설정을 주입하는 로봇입니다.
*/
module.exports = function withAndroidKeystoreProperties(config) {
return withAppBuildGradle(config, (config) => {
let contents = config.modResults.contents;
// 1. Gradle 상단 로더 주입
const loaderLogic = `
// [system builder] Keystore Properties Loader
def keystorePropertiesFile = rootProject.file("../keystore.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
`;
if (!contents.includes("keystorePropertiesFile")) {
contents = contents.replace(
/(apply plugin: "com\.facebook\.react")/,
`$1\n${loaderLogic}`,
);
}
// 2. signingConfigs 통째로 교체
const signingConfigsBlock = `
signingConfigs {
debug {
if (keystorePropertiesFile.exists()) {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
} else {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
release {
if (keystorePropertiesFile.exists()) {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
}`;
contents = contents.replace(
/signingConfigs\s*\{[\s\S]*?\}\s*buildTypes/g,
`${signingConfigsBlock.trim()}\n buildTypes`,
);
// 3. release 빌드 타입 연결 수정
contents = contents.replace(
/release\s*\{[\s\S]*?signingConfig\s*signingConfigs\.debug/g,
(match) =>
match.replace("signingConfigs.debug", "signingConfigs.release"),
);
config.modResults.contents = contents;
return config;
});
};
트러블슈팅: EAS 빌드와 환경 설정
플러그인 완성 후에도 한 가지 문제가 더 있었습니다. EAS Build는 기본적으로 .gitignore에 등록된 파일을 클라우드로 전송하지 않습니다. 이 때문에 keystore 파일과 설정값들이 누락되어 빌드 실패가 발생했었죠.
이 과정에서 EAS 빌드 플로우를 면밀히 분석했고, 클라우드 환경에서도 안전하게 파일을 참조할 수 있도록 빌드 스크립트를 보완했습니다. 결과적으로 로컬 프리빌드와 클라우드 자동 배포 모두 성공적인 결과를 얻었습니다.
기술적 성찰
이번 여정은 단순히 구글 로그인 에러를 고치는 것 이상의 가치가 있었습니다.
| 단계 | 주요 작업 | 결과 |
| 초기 단계 | 수동 Gradle 수정 | 프리빌드 시 설정 증발 |
| 탐색 단계 | Config Plugin 학습 | 네이티브 코드 제어권 획득 |
| 최적화 단계 | system builder 통합 | 빌드 자동화 및 파이프라인 완성 |
플러그인을 자유롭게 조립할 수 있다는 깨달음은 Expo 개발자에게 엄청난 자유도를 부여합니다. 이제 저는 어떤 복잡한 네이티브 설정도 두렵지 않은 시스템을 갖추게 되었습니다.
저와 같은 고민을 하던 개발자분들에게 이 글이 명쾌한 해답이 되었길 바랍니다.