React Native Debug·Release 구성 차이: No script URL provided 가 뜨는 이유
앱 두 개를 아이폰에 올려놓고 하나를 열었더니 빨간 화면에 이 문구가 떴습니다.
No script URL provided빌드는 성공했고 설치도 됐는데 앱이 안 열립니다. 원인을 찾다 보니 Debug와 Release 구성의 차이를 제대로 이해하지 못하고 있었다는 걸 알았습니다.
JS가 앱 안에 없습니다
React Native 앱에서 Debug 빌드는 자바스크립트를 앱에 넣지 않습니다. 실행할 때마다 Metro 서버에 접속해서 받아옵니다.
그래서 맥에서 Metro가 돌고 있어야 앱이 열립니다. 맥을 끄면 폰에 설치된 앱이 안 열립니다. 저 에러는 "받아올 주소를 모르겠다"는 뜻입니다.
Release는 반대입니다. 빌드할 때 JS를 통째로 묶어 main.jsbundle로 앱 안에 넣습니다. 게다가 요즘 Expo는 Hermes를 기본 엔진으로 쓰기 때문에, 텍스트가 아니라 미리 컴파일한 바이트코드를 넣습니다. 실행 시점에 파싱할 게 없으니 앱 시작도 빠릅니다.
app.json에 jsEngine을 안 적어뒀다면 Hermes입니다. Podfile을 보면 이렇게 되어 있습니다.
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil ||
podfile_properties['expo.jsEngine'] == 'hermes',더 고약한 경우는 따로 있습니다
No script URL provided는 오히려 친절한 편입니다. 접속할 주소를 못 정했으니 그냥 멈추기 때문입니다.
오전에 겪은 건 이랬습니다. 앱은 멀쩡히 열렸는데 다른 앱의 화면이 떴습니다. 그다음 이런 에러가 났습니다.
Cannot find native module 'ExpoSecureStore'Metro 기본 포트가 8081인데, 다른 프로젝트의 Metro가 그 포트를 잡고 있었습니다. 앱은 8081에 접속해서 JS를 받아왔고, 그게 남의 앱 코드였습니다. 그 코드가 요구하는 네이티브 모듈이 이 앱에는 없으니 저 에러가 난 겁니다.
접속이 되기 때문에 더 헷갈립니다. 에러 메시지만 보면 모듈 설치 문제로 보이는데 실제로는 번들을 잘못 받은 것입니다.
Metro는 여러 개 띄울 수 있습니다
앱마다 포트를 나누면 됩니다.
cd app-a && npx expo run:ios --device <UDID> --port 8081
cd app-b && npx expo run:ios --device <UDID> --port 8082중요한 건 포트가 빌드 시점에 앱 안에 박힌다는 점입니다. --port 8082로 빌드한 앱은 그 뒤로 8082만 찾습니다. 나중에 Metro를 8081에 띄우면 그 앱은 못 찾습니다.
그래서 앱마다 포트를 정해두고 그 포트로 빌드해두는 편이 낫습니다. 저는 포트를 안 나눠 쓰다가 오늘 두 번 당했습니다.
두 구성의 차이 전체
| Debug | Release | |
|---|---|---|
| JS 위치 | Metro 서버 | 앱 내부, Hermes 바이트코드 |
| 맥 없이 실행 | 안 됨 | 됨 |
| Fast Refresh | 있음 | 없음 |
| 개발자 메뉴·인스펙터 | 있음 | 없음 |
| 에러 표시 | 빨간 화면 | 그냥 종료 |
| 네이티브 최적화 | 없음 | 켜짐 |
| JS 미니파이 | 안 함 | 함 |
| 빌드 시간 | 짧음 | 김 |
| 스토어 제출 | 불가 | 이것으로 나감 |
성능 차이가 생각보다 큽니다. Debug에서 리스트 스크롤이 버벅인다고 성능 문제로 판단하면 안 됩니다. 네이티브가 최적화 없이 컴파일되고 JS도 미니파이되지 않은 상태라 원래 느립니다. 최적화는 Release에서 측정해야 의미가 있습니다.
Release에서만 나는 버그가 있습니다
이게 실무에서 제일 중요한 부분입니다.
__DEV__로 감싼 코드는 Release에서 통째로 사라집니다. 거기에 필요한 로직이 들어가 있으면 Release에서만 동작이 달라집니다.
미니파이는 함수와 클래스 이름을 바꿉니다. 이름에 의존하는 코드가 있으면 깨집니다. 안드로이드는 R8까지 걸려서 리플렉션을 쓰는 라이브러리가 문제를 일으킵니다.
그래서 스토어에 올리기 전에 반드시 Release로 실기기 테스트를 해야 합니다. Debug에서 잘 돌던 게 심사에서 크래시로 돌아오는 경우가 대개 여기서 나옵니다.
EAS 프로필도 같은 구분을 씁니다. development는 Debug에 개발 클라이언트를 얹은 것이고, preview와 production은 Release입니다. 스토어에 올라가는 건 언제나 Release입니다.
어느 쪽을 쓸 것인가
기준은 단순합니다. 코드를 자주 고치는 중이면 Debug, 폰에 두고 그냥 쓸 거면 Release.
개발 중에는 Fast Refresh가 주는 이득이 압도적입니다. 화면을 고치고 저장하면 바로 반영되니 빌드를 다시 할 필요가 없습니다. 오늘 점검 화면을 여러 번 고쳤는데 재빌드는 한 번도 안 했습니다.
반대로 만든 앱을 폰에 넣어두고 쓰려는 순간부터는 Metro에 묶여 있으면 안 됩니다. 맥을 켜둬야 열리는 앱은 앱이 아닙니다.
저는 이 경계를 늦게 알아서, 앱이 안 열리는 걸 빌드 실패로 오해하고 한참 헤맸습니다.
함께 읽기
- Expo 앱을 통째로 복사해 새 프로젝트 만들기: 그대로 가는 파일과 고쳐야 하는 파일성경 지도라는 앱을 만들면서 npx create-expo-app을 치지 않았습니다. 이미 두 스토어에 배포되고 있는 다른 앱의 폴더를 통째로 복사해서 시작했습니다.
- Metro 포트 충돌로 배운 것: RN이 OTA 업데이트를 할 수 있는 이유새로 만든 앱을 아이폰에 설치했습니다. 빌드는 성공했고 설치도 됐는데, 앱을 열자 빨간 에러 화면이 떴습니다. 그런데 에러가 가리키는 파일이 이상했습니다.
- Watchman은 React Native 개발에서 왜 필요할까React Native나 Expo로 개발하다 보면 코드를 고쳤는데 앱에 바로 반영되지 않는 경우가 있습니다. 이때 원인 중 하나가 파일 변경 감시 도구인 Watchman 문제일 수 있습니다.
- Liquid Glass 아이콘 직접 만들기: .icon 파일 구조와 librsvg 함정앱 아이콘 하나 만드는 데 하루를 썼습니다. 열 번 넘게 갈아엎었고 빌드를 네 번 중간에 끊었습니다. 그 과정에서 알아낸 것 중에 검색해도 잘 안 나오는 게 두 개 있어서 적어둡니다.
- 빌드 우회 셋 정리: 어디까지가 내가 고칠 수 있는 문제인가앱 하나를 두 스토어에 올리는 동안 빌드가 여러 번 깨졌고, 그때마다 우회를 하나씩 넣었습니다. 다 끝나고 저장소를 보니 임시방편이 셋 쌓여 있었습니다.