RN 앱 두 번째 배포: 재사용되는 인증서, 새로 해야 하는 등록
몇 주 전에 만든 고객 포털 앱을 반쯤 복사해서 새 RN 앱을 하나 더 만들었습니다. 성경 속 여정을 지도에 그리는 화면 하나를 웹에서 옮겨온 것뿐인데, 옮기고 나니 "인증서까지 그대로 가져다 쓸 수 있나"가 궁금해졌습니다. 결론부터 적으면 절반만 맞았습니다.
스캐폴드부터 새로 하려다 멈췄습니다
처음엔 create-expo-app으로 빈 프로젝트를 새로 만들 생각이었습니다. 폴더까지 파 놓은 상태에서 "어차피 기존 앱이 이미 스토어에 낼 수 있는 상태인데, 그거 그대로 복사해오면 안 되냐"는 말을 듣고 멈췄습니다. 맞는 말이었습니다. 빈 템플릿에서 시작하면 eas.json 빌드 프로필, GitHub Actions 워크플로 다섯 개, .gitignore의 서명 파일 제외 규칙을 전부 손으로 다시 적어야 합니다. 이미 한 번 실전에서 검증된 설정을 버리고 처음부터 다시 쓸 이유가 없었습니다.
그래서 기존 앱 저장소를 rsync로 통째로 복사했습니다. node_modules, .git, 빌드로 생성되는 네이티브 폴더, 서명 키가 든 폴더만 빼고요. 복사한 다음엔 기존 앱의 로그인 화면·프로젝트 관리 화면·API 클라이언트 코드를 걷어내고, 지도 화면과 데이터 파일을 채워 넣었습니다. 새 저장소의 첫 커밋이 파일 32개, 1만 줄 남짓이었는데, 그중 실제로 새로 쓴 코드는 지도 화면 하나와 데이터 파일 몇 개뿐이고 나머지는 전부 기존 앱에서 온 배포 뼈대였습니다.
그대로 가져온 것
- GitHub Actions 워크플로(타입체크, EAS 빌드, OTA 업데이트):
secrets: inherit패턴이라 이름만 맞으면 그대로 동작합니다 eas.json의 빌드 프로필 구조(개발/미리보기/정식 세 갈래).gitignore의 서명 파일·비밀 키 제외 규칙package.json의 핵심 의존성 버전(Expo SDK, React Native 버전)- Apple Developer 팀 계정
Apple Developer Program은 연 $99를 내는 게 앱 단위가 아니라 계정(팀) 단위입니다. 같은 팀 아래 앱을 몇 개 등록하든 추가 비용이 없습니다. 그래서 "새 앱이니까 개발자 계정도 새로 파야 하나" 고민했는데, 그럴 필요가 없었습니다. 배포 인증서도 팀 단위로 발급되기 때문에 마찬가지로 재사용됩니다.
그런데 앱마다 새로 해야 하는 것도 있었습니다
인증서와 계정은 팀 단위인데, 그 아래 앱 하나하나는 각자 등록해야 하는 항목이 따로 있습니다.
- EAS 프로젝트 ID: 앱마다 고유합니다.
eas-cli init을 새로 돌려야 발급됩니다. - App Store Connect 앱 레코드: 번들 ID를 등록하고 앱을 하나 새로 만들어야 앱 고유 ID를 알 수 있습니다. 이게 없으면 자동 제출 설정 자체를 쓸 수 없습니다.
- Google Play Console 앱: 마찬가지로 새로 등록해야 합니다. 기존 앱 배포에 쓰던 서비스 계정 키가 있긴 한데, 그 키가 새 앱에 대한 권한을 자동으로 갖는 게 아닙니다. Play Console에서 그 서비스 계정에 새 앱의 배포 권한을 따로 부여해야 알 수 있습니다.
- 프로비저닝 프로파일: 인증서는 팀 단위지만 프로비저닝은 번들 ID 단위라 앱마다 새로 발급됩니다. 다행히 이건 EAS가 빌드할 때 알아서 만들어줍니다.
- CI 시크릿: 저장소가 다르면 시크릿도 다시 등록해야 합니다. 기존 저장소에 넣어둔 토큰이 새 저장소로 자동으로 넘어오지 않습니다.
Android 지도에서 한 번 걸릴 뻔한 부분
처음엔 react-native-maps를 썼습니다. 그런데 타일 이미지 자체는 CARTO에서 무료로 받아 오버레이로 얹는데도, 바탕 지도 컴포넌트는 기본값이 Google Maps라서 Android에서는 Google Maps API 키가 없으면 지도가 아예 안 뜬다는 걸 알게 됐습니다. iOS는 Apple Maps가 기본이라 이 키가 필요 없습니다. 같은 코드인데 플랫폼에 따라 등록해야 하는 게 다른 셈이었습니다.
결국 그 키를 받지 않기로 했습니다
Google Maps API 키를 그냥 발급받고 넘어가려다가, 이 앱을 처음 만들 때 정해둔 원칙이 걸렸습니다. 서버도, API 키도 없는 앱으로 시작한 프로젝트인데, Android 지도 하나 때문에 그 원칙이 깨지는 셈이었습니다. Google Maps Platform은 무료 한도 안에 있어도 결제 카드 등록이 전제라, 예전에 구글 지도를 포기하고 Leaflet과 OpenStreetMap으로 갔을 때와 똑같은 문제가 다시 나온 것이기도 했습니다.
그래서 react-native-maps 대신 MapLibre(오픈소스 지도 렌더러)로 바꿨습니다. CARTO 타일을 raster 소스로 직접 얹는 구조라 iOS·Android 둘 다 키가 필요 없습니다. 지도 데이터 출처는 그대로 OpenStreetMap입니다. 마커·선 그리는 로직은 다시 짰지만 화면에 보이는 결과물은 거의 똑같습니다. 다만 육로/바닷길을 점선으로 구분하는 부분에서 한 번 더 걸렸습니다. 선의 점선 스타일을 구간(feature)별로 다르게 주는 게 이 렌더러에서는 안 먹혀서, 여정 하나를 육로용 소스와 바닷길용 소스 둘로 나눠 그리는 쪽으로 구조를 바꿔야 했습니다.
계정을 착각했습니다
지도를 다 고치고 EAS 프로젝트를 새로 만들려는데 이번엔 다른 데서 걸렸습니다. GitHub 저장소는 전부 같은 계정 아래 있어서, 설정 파일의 소유자 값도 당연히 같은 계정일 거라고 생각했습니다. 로그인한 채로 프로젝트 생성 명령을 돌렸더니 "그 계정에 프로젝트를 만들 권한이 없다"는 에러가 났습니다.
확인해보니 기존 앱의 EAS 프로젝트를 조회하는 것조차 권한 오류가 났습니다. GitHub 계정과 Expo 계정이 서로 다른 로그인이었던 겁니다. 로그아웃하고 실제 소유 계정으로 다시 로그인하고 나서야 프로젝트가 만들어졌습니다. 조직 이름이 같다고 계정도 같은 게 아니라는 걸, 이번에도 직접 걸려보고서야 알았습니다.
지금 상태
준비 작업은 사실상 끝났습니다. EAS 프로젝트 생성과 OTA 설정, 두 스토어의 앱 레코드 등록, CI 토큰 등록까지 마쳤습니다. 남은 건 Play 서비스 계정 키를 EAS에 올리는 것 하나이고, 그 다음은 첫 빌드입니다.
재사용 가능한 층(팀 계정·인증서·워크플로 구조)과 앱마다 새로 해야 하는 층(프로젝트 ID·스토어 앱 레코드·프로비저닝·CI 시크릿)이 갈린다는 처음 정리는 맞았습니다. 다만 그 위에 예상 못 한 층이 하나 더 있었습니다. 계정 자체를 착각하는 경우입니다.
이번 작업에서 두 번 걸렸습니다. 한 번은 저장소 계정과 Expo 계정을 같은 것으로 봤고, 다른 한 번은 Play 서비스 계정 키 파일명에 적힌 프로젝트 이름을 그 키의 소속으로 봤습니다. 둘 다 이름이 비슷해서 같은 소속이라고 넘겨짚은 경우였습니다. 재사용 가능한지 따지기 전에, 그게 애초에 어느 계정 소속인지부터 확인했어야 했습니다.
우리 서비스에 적용할 계획이 있으신가요?
모바일 앱 개발 살펴보기 →함께 읽기
- 바이너리 등록 후 남은 것들: 자동화되는 일과 사람이 해야 하는 일빌드가 끝나고 양쪽 스토어 콘솔에 바이너리가 올라갔습니다. 저는 그때 "이제 다 됐다"고 생각했습니다. 실제로는 거기서부터가 절반이었습니다.
- Expo 앱 스토어 배포 준비 체크리스트: 순서가 중요한 이유새 Expo 앱 하나를 두 스토어에 올릴 준비를 했습니다. 필요한 항목 자체는 검색하면 다 나옵니다. 그런데 정작 저를 막은 건 항목이 아니라 순서였습니다. 앞 단계가 끝나야 뒤 단계의 값이 생기는 구간이 몇 군데 있고, 그걸 모르고 덤비면 "왜 이 필드가 비어 있지"에서 멈춥니다.
- 모바일 앱 스토어 배포 자동화를 설계할 때 볼 것들모바일 앱 배포는 빌드 파일을 만드는 것에서 끝나지 않습니다. 인증서, 서명 키, 스토어 메타데이터, 심사 정보, 버전 관리, 제출 자동화까지 함께 설계해야 반복 가능한 배포가 됩니다.
- 같은 Expo 앱인데 배포 방식은 달랐습니다: iOS와 Android 심사 제출 비교기앞선 글에서는 이미 TestFlight에 올라간 iOS 빌드를 AI가 App Store 심사까지 제출한 과정을 다뤘습니다.
- App Store Connect와 Google Play API 비교: 인증·권한·한계앱 하나를 두 스토어에 등록하면서 양쪽 API를 같은 날 연달아 만졌습니다. 목적이 똑같았기 때문에 차이가 잘 보였습니다. 인증 방식부터 다르고, 권한을 다루는 사고방식이 다르고, 심지어 "안 되는 걸 알려주는 방식"까지 달랐습니다.