Expo dev-client로 안드로이드 실기기에 앱 올리고 Metro 붙이기
3부작 「맥에서 Xcode 없이 안드로이드 앱 실기기 테스트하기」 중 2부입니다. 1부 「맥에서 Xcode 없이 안드로이드 개발환경 세팅하기 (Homebrew만으로)」에서 이어집니다.
1부에서 Homebrew만으로 안드로이드 개발환경을 세팅했으니, 이제 Expo 앱을 실기기에 올리고 Metro를 붙이는 일만 남았습니다. 간단할 줄 알았는데 몇 군데서 막혔고, 막힐 때마다 원인이 제 예상과 달랐습니다. 실기기 빌드부터 pnpm 모노레포의 Metro 설정까지, 막혔던 지점을 순서대로 적어 둡니다.
ios/android 폴더는 소스가 아니라 생성물입니다
처음 저지른 실수는 네이티브 파일을 직접 고친 것이었습니다. 권한 하나를 추가하려고 AndroidManifest.xml을 손으로 수정했는데, 나중에 prebuild --clean을 한 번 돌리자 그 수정이 통째로 사라졌습니다.
Expo에는 CNG(Continuous Native Generation)라는 원리가 있습니다. ios/와 android/ 디렉토리는 app.json과 config plugin으로부터 매번 새로 생성되는 산출물이라는 뜻입니다. 그래서 이 두 폴더는 .gitignore에 넣고 정본은 app.json 하나로 둡니다. 권한이나 값도 네이티브 파일이 아니라 app.json의 permissions·infoPlist·plugins로 표현해야 prebuild --clean 재생성에도 살아남습니다.
첫 빌드는 7~8분, 그다음부터는 빠릅니다
실기기에 올리는 명령 자체는 한 줄입니다.
npx expo run:android # = npm run android
처음 이 명령을 돌렸을 때 7~8분이 걸렸습니다. 멈춘 줄 알고 중간에 끊을 뻔했는데, 그동안 gradle 배포판을 내려받고 의존성을 받고 NDK의 CMake로 네이티브 코드를 컴파일하는 중이었습니다. 한 번 캐시가 쌓이면 증분 빌드는 1분 20초 정도로 짧아집니다. 빌드가 끝나면 APK가 실기기에 설치되고, dev-client가 실행되면서 Metro에 붙습니다.
모노레포에서 Metro가 모듈을 못 찾을 때
여기서 가장 오래 막혔습니다. 빌드는 성공했는데 앱을 켜자 Unable to resolve module로 번들이 실패했습니다. 원인은 pnpm의 구조에 있었습니다.
pnpm은 react·react-native·expo 같은 공유 의존성을 저장소 루트의 node_modules에 hoisting하고 .pnpm/ 심볼릭링크로 배치합니다. 그런데 Expo가 기본으로 깔아 주는 Metro 설정은 앱 자기 자신의 node_modules만 바라봅니다. 그래서 루트로 올라간 의존성을 찾지 못하는 것이었습니다. metro.config.js에 저장소 루트를 알려 주는 두 줄이 없으면 계속 실패합니다.
const workspaceRoot = path.resolve(projectRoot, '../../..'); // 모노레포 루트
config.watchFolders = [workspaceRoot];
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
참고로 Metro 0.82(React Native 0.81에 동봉) 이상은 심볼릭링크를 기본으로 해소합니다. 그래서 .pnpm/ 링크는 자동으로 resolve되고, 우리가 직접 챙길 것은 위의 watchFolders와 nodeModulesPaths 두 가지입니다.
실기기는 LAN IP로 붙습니다
Metro는 개발용 서버라 실기기가 네트워크로 접속해야 합니다. 폰과 맥이 같은 Wi-Fi 서브넷에 있어야 하고, Metro를 LAN에 노출시켜 띄웁니다.
EXPO_PUBLIC_API_URL=http://192.168.0.10:4000 npx expo start --dev-client --port 8081
여기서 192.168.0.10은 맥의 LAN IP, 4000은 백엔드 포트입니다. IP가 바뀌면 이 환경변수만 갱신하면 되고, 다시 빌드할 필요는 없습니다. 재빌드 없이 env만 바꾸면 된다는 점이 실기기 테스트에서 은근히 요긴했습니다.
런처가 안 붙을 때는 딥링크로
dev 런처에는 최근 접속 목록("Recently Opened")이 뜨는데, 이게 비어 있거나 자동 탐색이 안 되는 경우가 있었습니다. 그럴 때는 adb로 딥링크를 직접 던져 Metro에 붙일 수 있습니다.
adb shell am start -a android.intent.action.VIEW \
-d "exp+myapp://expo-development-client/?url=http%3A%2F%2F192.168.0.10%3A8081"
-d 뒤의 값은 퍼센트 인코딩된 http://192.168.0.10:8081입니다. 런처 UI를 거치지 않고 바로 특정 Metro 주소로 붙으니, 목록이 비어 있어도 문제가 되지 않았습니다.
화면이 멈췄을 때는 콜드 스타트
앱이 흰 화면에서 멈추거나 상태가 꼬였을 때, 앱을 완전히 죽였다가 다시 띄우면 대부분 풀렸습니다.
adb shell am force-stop com.example.myapp
adb shell am start -n com.example.myapp/.MainActivity
com.example.myapp은 앱의 패키지 이름입니다. force-stop으로 프로세스를 완전히 내리고 MainActivity로 다시 시작하면, 어정쩡하게 살아 있던 상태가 초기화됩니다.
다음 편으로
마지막으로 사소하지만 몇 번 당한 것이 있습니다. Metro나 백엔드를 배경으로 돌리려고 nohup pnpm dev &를 썼는데, pnpm이 자식 프로세스를 detach하면서 정작 서버가 죽는 경우가 있었습니다. 서브셸로 감싸거나, 쓰는 도구가 제공하는 background 실행 옵션을 쓰는 편이 안전했습니다.
돌이켜 보면 막힘의 대부분은 '실기기는 네트워크 너머의 다른 컴퓨터'이고 'ios/android는 생성물'이라는 두 원리를 몸으로 이해하지 못해서였다는 생각이 들었습니다. 3부에서는 이렇게 붙인 실기기를 adb로 원격 조작하면서 자동 QA하는 법을 다루겠습니다.
3부작 중 2부였습니다. 이어지는 3부는 「adb로 안드로이드 실기기를 원격 조작하며 QA하기」입니다. (1부: 「맥에서 Xcode 없이 안드로이드 개발환경 세팅하기 (Homebrew만으로)」)
관련 글
맥에서 Xcode 없이 안드로이드 개발환경 세팅하기 (Homebrew만으로)
iOS는 Xcode가 필수지만 안드로이드는 다릅니다. Android Studio 없이 Homebrew와 command-line tools만으로 React Native/Expo 개발환경을 세팅한 기록입니다. JDK 17 keg-only 함정부터 SDK 패키지, 에뮬레이터까지 막힌 순서 그대로 정리했습니다.
Xcode 설치부터 아이폰 실기기 설치까지, 무료 계정으로 뚫은 관문 여섯 개
Xcode를 설치했는데도 아이폰에 앱이 올라가지 않았습니다. 커맨드라인 도구와 Xcode의 차이부터 무료 개발자 계정의 제약, 개발자 모드가 켜지지 않는 이유까지, 실제로 막혔던 여섯 지점과 원문 에러를 정리했습니다.
Bun으로 갈아탈까? 실제 모노레포로 검증해본 결과
1633개 패키지를 가진 실제 프로덕션 모노레포에서 Bun 마이그레이션을 검토했습니다. 네이티브 모듈 호환성부터 체감 속도 예측, 워크트리 설치 시간 실측까지, 벤치마크 숫자가 아닌 현실적인 분석을 정리했습니다.