Expo CNG: android 와 ios 폴더가 리포에 없는 이유
Expo 로 만든 앱의 리포를 클론하면 ios/ 도 android/ 도 없습니다. .gitignore 를 열어 보면 아예 막혀 있습니다.
/ios/
/android/그런데 그 리포에서 나온 앱은 스토어에 올라가 있습니다. 네이티브 프로젝트 없이 네이티브 앱이 나온 셈입니다.
아직 안 만든 것이라는 오해
흔한 이해는 두 가지입니다. 아직 네이티브 코드를 건드릴 일이 없어서 안 만든 것이거나, 예전 Expo 의 eject 를 아직 안 한 것이라는 쪽입니다. 둘 다 "언젠가 만들면 그때부터 내 소스" 라는 그림을 전제합니다.
그 그림으로는 설명되지 않는 것이 있습니다. npx expo prebuild 를 돌리면 ios/ 가 생기는데, 한 번 더 돌리면 또 생깁니다. 지우고 다시 돌려도 같은 것이 나옵니다. 소스라면 이럴 이유가 없습니다.
소스가 아니라 산출물인 네이티브 프로젝트
Expo 는 이 방식을 Continuous Native Generation 이라고 부릅니다. 문서의 표현을 그대로 옮기면, 네이티브 프로젝트는 "필요할 때만 생성되는 짧은 수명" 을 갖고, 개발자는 "네이티브 프로젝트 코드 전부가 아니라 자기 커스터마이징의 정의만" 유지합니다.
정본은 app.json 과 config plugin 입니다. ios/ 는 그것을 컴파일러가 읽을 수 있는 형태로 펼쳐 놓은 결과입니다. 컴파일러는 app.json 을 모르고 .xcodeproj 만 알기 때문에 그 변환이 필요합니다.
node_modules 와 같은 자리라고 보면 대체로 맞습니다. package.json 이 정본이고 node_modules 는 그것을 펼친 결과이며, 지우고 다시 만들어도 같은 것이 나오고, 그래서 커밋하지 않습니다.
안드로이드도 똑같습니다. npx expo prebuild -p android 를 돌리면 android/ 가 같은 방식으로 생깁니다. 한쪽만 특별하지 않습니다.
손으로 고친 것이 사라지는 이유
이 구분이 실무에서 값을 치르는 자리는 여기입니다. Xcode 를 열어 서명 설정을 바꾸거나 build.gradle 을 손보면 그 순간에는 됩니다. 다음에 SDK 를 올리거나 플러그인을 추가하면서 prebuild 를 다시 돌리면 원래대로 돌아갑니다.
바꿀 것이 있으면 결과물이 아니라 생성 과정에 넣어야 합니다. 그 자리가 config plugin 입니다. 플러그인은 두 시점에 끼어듭니다. 앱 설정을 평가할 때 한 번, 그리고 prebuild 가 네이티브 파일을 쓰는 동안 한 번입니다. 뒤쪽이 우리가 필요로 하는 시점입니다. 매번 생성될 때마다 같은 수정이 다시 적용되기 때문입니다.
--clean 없이 돌릴 때 갈리는 결과
prebuild 는 기본적으로 기존 파일 위에 변경을 얹습니다. 빠른 대신, 문서가 적어 둔 대로 "경우에 따라 같은 결과가 나오지 않을 수 있습니다". --clean 은 기존 네이티브 디렉터리를 지우고 처음부터 만듭니다.
무엇이 남고 무엇이 덮이는지는 파일마다 다릅니다. 문서도 그 경계를 나열하지 않고 다를 수 있다고만 말합니다. 저는 이 부분을 추측하지 않는 편이 낫다고 봅니다. 플러그인이 실제로 먹는지 확인할 때는 --clean 으로 돌려서 보는 쪽이 확실합니다.
커밋하는 순간 소스가 되는 지점
문서는 네이티브 디렉터리를 .gitignore 에 넣는 것을 기본으로 두면서도, 커밋해서 쓰는 경우를 함께 설명합니다. 플러그인이 없는 네이티브 수정이 필요하거나 빌드 파이프라인이 그것을 요구할 때입니다.
그 선택을 하면 CNG 를 반쯤 끄는 것입니다. 그때부터는 SDK 를 올릴 때마다 생성된 결과와 손으로 고친 것을 사람이 합쳐야 합니다. 저라면 플러그인으로 표현할 방법을 먼저 찾고, 정말 안 될 때만 커밋 쪽으로 넘어가겠습니다. 되돌아오기가 쉽지 않은 방향이라서 그렇습니다.
설계에서 무엇이 달라지나
네이티브 폴더를 산출물로 보면 판단이 몇 가지 따라옵니다. 리포에 없는 것은 결함이 아니라 정상입니다. 빌드 기계에는 prebuild 를 돌릴 수 있는 환경이 있어야 합니다. 그리고 "네이티브를 고쳤다" 는 말은 app.json 이나 플러그인을 고쳤다는 뜻이어야 하며, 그렇지 않다면 그 수정은 다음 생성까지만 사는 것입니다.
클라우드 빌드를 떠나 로컬에서 빌드하려 할 때 이 구분이 먼저 걸립니다. 그 준비에서 걸린 다른 항목들은 따로 적었습니다.
함께 읽기
- 같은 Expo 앱인데 배포 방식은 달랐습니다: iOS와 Android 심사 제출 비교기앞선 글에서는 이미 TestFlight에 올라간 iOS 빌드를 AI가 App Store 심사까지 제출한 과정을 다뤘습니다.
- Expo의 역사: SDK 0에서 CNG와 개발 빌드까지Expo를 설명할 때 가장 자주 붙는 말은 "React Native를 쉽게 쓰게 해 주는 도구"입니다. 틀린 설명은 아니지만 지금의 Expo를 이해하기에는 범위가 너무 좁습니다. Expo는 미리 만들어진 앱에서 JavaScript를 실행하던 초기 경험을 출발점으로 삼았고, 지금은 네이티브 프로젝트 생성과 모듈 개발, 라…
- Expo SDK 56 iOS 빌드 오류와 Swift weak let 호환성저는 견적킷을 iPhone에서 실행하려다 xcodebuild 코드 65를 만났습니다. 빌드 화면에는 Reanimated와 Worklets 컴파일 로그가 길게 지나갔고, 마지막에는 오류 15개가 있다는 메시지만 남았습니다.
- 쓰지 않는 iOS 권한 문구 때문에 App Store 자동 검사에서 반려되는 이유앱스토어 심사라고 하면 사람이 아이폰을 들고 앱을 하나씩 눌러 보는 장면부터 떠올리기 쉽습니다. 저도 그랬습니다. 그런데 iOS 앱을 제출한 뒤 받은 반려 메시지에는 automated analysis라는 표현이 적혀 있었습니다. 사람 심사관의 판단을 기다리기도 전에 업로드한 바이너리의 권한 문구가 자동 분석에서 걸린 경…
- Expo에서 app.json 수정이 OTA 업데이트에 반영되지 않는 이유Expo 앱의 app.json을 고친 뒤 저장소에 푸시했습니다. production 채널의 OTA 작업도 평소처럼 실행됐습니다. 배포 작업이 성공하면 변경 사항이 앱에 들어간 것처럼 느껴집니다. 이번에는 그렇지 않았습니다.