RSS

Expo 앱을 통째로 복사해 새 프로젝트 만들기: 그대로 가는 파일과 고쳐야 하는 파일

성경 지도라는 앱을 만들면서 npx create-expo-app을 치지 않았습니다. 이미 두 스토어에 배포되고 있는 다른 앱의 폴더를 통째로 복사해서 시작했습니다.

이유는 단순합니다. 새 프로젝트를 만들면 코드는 생기지만 배포되는 상태는 안 생깁니다. EAS 프로젝트 연결, 빌드 프로필, GitHub Actions 워크플로, 인증서 규약, .gitignore 관례. 이걸 처음부터 다시 맞추는 게 화면 하나 짜는 것보다 오래 걸린다는 걸 지난번에 겪었습니다.

결과적으로는 옳은 선택이었는데, 복사가 어디까지 통하고 어디서부터 안 통하는지가 명확히 갈렸습니다. 다 끝난 뒤에 두 프로젝트를 파일 단위로 비교해봤습니다.

그대로 9 / 수정 16 / 신규 9 / 원본에만 21

원본은 추적 파일 46개, 새 앱은 34개입니다.

그대로 넘어간 9개

.claude/settings.json
.github/workflows/build-android.yml
.github/workflows/build-ios.yml
.github/workflows/ci.yml
.github/workflows/eas-build-reusable.yml
.gitignore
AGENTS.md
LICENSE
app.config.js

빌드 워크플로 네 개가 한 글자도 안 고치고 넘어갔습니다. 이게 복사로 번 것의 대부분입니다.

왜 그대로 통했는지가 중요합니다. 앱의 정체성을 파일 안에 적어두지 않았기 때문입니다. app.config.js가 대표적입니다.

module.exports = ({ config }) => ({
  ...config,
  name: IS_DEVELOPMENT ? `${config.name} Dev` : config.name,
  ios: {
    ...config.ios,
    bundleIdentifier: IS_DEVELOPMENT
      ? `${config.ios.bundleIdentifier}.dev`
      : config.ios.bundleIdentifier,
  },
  ...
});

개발 빌드와 프로덕션 빌드의 번들 ID를 나누는 파일인데, 앱 이름도 번들 ID도 직접 쓰지 않고 config에서 받아서 가공만 합니다. 그래서 어느 앱에 갖다 놓아도 그대로 동작합니다. 원래 이렇게 짤 의도로 짠 건 아니었고, 결과적으로 그렇게 되어 있었습니다.

한 줄만 고친 워크플로

워크플로 다섯 개 중 넷은 그대로였고 하나만 고쳤습니다. 차이가 딱 이 한 줄입니다.

-      - 'src/**'
+      - 'lib/**'

OTA 업데이트를 자동으로 내보내는 워크플로인데, 어떤 경로가 바뀌었을 때 실행할지를 적어둔 부분입니다. 원본은 소스를 src/에 두고 새 앱은 lib/에 뒀으니 안 맞습니다.

다섯 개 중 유일하게 경로를 하드코딩한 파일이 유일하게 손이 간 파일이었습니다. 앞에서 말한 기준이 그대로 드러납니다.

고쳐야 했던 16개

파일
app.json 앱 이름, 번들 ID, 패키지명. 정체성 그 자체
eas.json 제출 대상 ascAppId를 새 앱 것으로
package.json 프로젝트 이름과 의존성 조정
package-lock.json 위 변경의 결과
assets/*.png 7개 아이콘, 스플래시, 파비콘
app/_layout.tsx 원본은 인증 상태를 감싸는 껍데기였음
tsconfig.json 경로 별칭
CLAUDE.md, DEPLOY.md 문서
.github/workflows/ota-update.yml 위의 한 줄

정체성을 담은 파일과 눈에 보이는 자산, 이 둘입니다. 나머지 배관은 건드릴 게 없었습니다.

안 가져온 21개

원본은 프로젝트 관리 포털이라 로그인, 탭 구조, 프로젝트 상세, 문서, 환경변수, 스레드 화면이 있습니다. 새 앱은 지도 한 화면짜리라 전부 필요 없었습니다.

app/(tabs)/_layout.tsx
app/login.tsx
app/project/[id].tsx
...
src/core/api.ts
src/core/auth-context.tsx
src/core/auth.ts

그러니까 실제 순서는 "복사한 다음 지우기"였습니다. 껍데기만 남기고 화면을 걷어낸 뒤 app/index.tsx 하나를 새로 썼습니다. 남길 것을 고르는 것보다 지울 것을 고르는 게 판단하기 쉬웠습니다. 지우다 보면 무엇이 배포에 필요한 배관이고 무엇이 그 앱의 기능인지 저절로 갈립니다.

복사로 번 것과 못 번 것

번 것은 파일만이 아닙니다. Apple 배포 인증서와 프로비저닝 프로파일은 팀 단위라 그대로 재사용됩니다. Android 업로드 키도 EAS Credentials에 이미 있었습니다. Expo 계정과 프로젝트 소유 조직도 같습니다.

못 번 것은 스토어에 앱을 등록하는 일 전부입니다. App Store Connect의 앱 생성, 번들 ID 등록, Play Console의 앱 생성, 스토어 메타데이터, 스크린샷, 개인정보처리방침 URL, 콘텐츠 등급 설문. 이건 새 앱이면 처음부터 다시 합니다. 심지어 App Store Connect는 API로 앱 생성 자체가 막혀 있어서 웹 콘솔에서 손으로 만들어야 했습니다.

복사가 줄여주는 건 레포 안쪽이고, 스토어 계정 쪽은 그대로 남습니다. 체감상 절반쯤 벌었습니다.

복사는 나쁜 것도 같이 옮깁니다

eas.json을 고치다가 하나 발견했습니다. 원본에는 이런 줄이 있었습니다.

"serviceAccountKeyPath": "./asc/illowa-f4d41-724b57c4631f.json"

Play 제출에 쓰는 서비스 계정 키 경로인데, asc/.gitignore에 걸려 있습니다. 즉 이 파일은 제 노트북에만 있고 레포에는 없습니다. 키가 새지 않은 건 다행이지만, 다른 곳에서 클론하면 제출이 안 됩니다. 로컬 상태에 묶인 설정입니다.

새 앱에서는 이 줄을 지우고 키를 EAS Credentials에 올렸습니다. 서버가 들고 있으니 어디서 실행하든 됩니다.

복사를 안 했으면 이 문제를 몰랐을 겁니다. 남의 설정을 그대로 옮겨 붙이면서 한 줄씩 읽게 되니까 눈에 띄었습니다. 반대로 말하면, 읽지 않고 복사했으면 문제도 같이 복제됐을 겁니다.

다시 한다면

또 복사할 겁니다. 다만 순서를 바꾸겠습니다. 이번에는 복사하고 코드를 다 짠 다음에 eas.jsonapp.json을 손봤는데, 그 사이에 잘못된 번들 ID로 빌드를 한 번 돌렸습니다.

복사 직후에 정체성 파일부터 정리하는 게 맞습니다. app.json의 이름과 번들 ID, eas.json의 제출 대상, package.json의 프로젝트 이름. 세 파일입니다. 이것만 먼저 바꿔두면 나머지는 원래 앱의 배관이 그대로 굴러갑니다.