새 노트북으로 React Native(Expo) 프로젝트 옮겨서 빌드하기 — 내가 만난 4가지 함정
노트북을 바꿨더니 빌드가 안 된다
개발하다 보면 노트북을 바꾸는 순간이 옵니다. 저도 작업하던 React Native(Expo) 앱을 새 맥으로 옮겼습니다. 보통 이럴 때 가장 무거운 node_modules는 다시 받으면 되니까 빼고, 나머지를 통째로 복사하죠. 저도 그렇게 했습니다. 정확히는 node_modules를 뺀다고 했는데 결과적으로 node_modules와 네이티브 android/ 폴더가 같이 따라왔습니다.
그리고 평소처럼 빌드를 돌렸더니... 한 번에 네 개의 벽에 차례로 부딪혔습니다.
- Java(JDK)가 없다
- Gradle이
node를 못 찾는다 No variants exist라는 정체불명의 Gradle 에러- 설치는 됐는데 앱 목록에 앱이 안 보인다
하나하나가 처음 보면 "이게 뭐지?" 싶은 것들이라, 같은 상황을 만날 분들을 위해 진단부터 해결까지 순서대로 정리해 봤습니다. 환경은 macOS + Expo SDK 54 + React Native 0.81 + Android 실기기(USB 디버깅) 기준입니다.
📌 이 글은 **빌드편(2편)**입니다.
node_modules를 빼고 옮긴 프로젝트의 JS 의존성을 먼저 복원하는 과정은 1편 — node_modules 없이 옮긴 프로젝트 복원하기를 참고하세요.
사전 상황 정리
먼저 제 출발점은 이랬습니다.
- 프로젝트 폴더를 새 맥으로 복사 (
node_modules,android/포함) - Android Studio는 설치돼 있음
- 안드로이드 폰을 USB로 연결 (
adb devices에 정상으로 잡힘) - 릴리스 서명용 keystore(.jks)는 별도 백업 폴더에 보관
빌드 명령은 단순합니다. 프로젝트의 android/ 폴더에서 Gradle을 돌리는 거죠.
cd android && ./gradlew assembleRelease
그런데 이 한 줄이 끝까지 가는 데 한참 걸렸습니다.
Step 1. "Unable to locate a Java Runtime" — JDK가 없다
첫 에러는 이거였습니다.
The operation couldn't be completed. Unable to locate a Java Runtime.
Please visit http://www.java.com for information on installing Java.
새 노트북이라 JDK가 아예 없었던 겁니다. 그렇다고 java.com에서 JDK를 따로 받을 필요는 없습니다. Android Studio를 설치했다면 그 안에 JDK(JBR, JetBrains Runtime)가 이미 들어 있거든요.
맥에서는 보통 이 경로에 있습니다.
ls "/Applications/Android Studio.app/Contents/jbr/Contents/Home"
이걸 JAVA_HOME으로 지정해서 Gradle을 돌리면 됩니다.
JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" \
./gradlew assembleRelease
💡 시스템에 JDK가 있는지 한 번에 보고 싶다면:
/Library/Java/JavaVirtualMachines/폴더를 보거나, Homebrew로 깔았다면/opt/homebrew/opt/openjdk*를 확인하면 됩니다. 둘 다 없으면 Android Studio JBR이 가장 손쉬운 답입니다.
Step 2. "Cannot run program node" — Gradle이 node를 못 찾는다
JDK를 잡아주니 이번엔 이 에러가 나왔습니다.
Cannot run program "node" ... error=2 (No such file or directory)
당황스러웠던 건, 분명 제 노트북엔 node가 깔려 있었다는 점입니다. node -v도 잘 됐고요. 그런데 왜 Gradle은 못 찾을까요?
이유는 Expo의 autolinking 때문입니다. RN 0.81 + Expo의 android/settings.gradle을 열어보면 이런 코드가 있습니다.
providers.exec {
commandLine("node", "--print",
"require.resolve('@react-native/gradle-plugin/...')")
}
빌드 과정에서 Gradle이 직접 node를 실행해 의존성 경로를 알아내는 구조입니다. 즉, Gradle을 실행하는 셸의 PATH에 node가 있어야 합니다.
제가 node를 못 잡은 진짜 원인은 nvm이었습니다. nvm으로 node를 설치하면, node 실행 파일은 ~/.nvm/versions/node/<버전>/bin 안에 있는데, 이 경로는 ~/.zshrc에서 nvm 초기화 스크립트를 불러올 때 비로소 PATH에 올라옵니다. 새 노트북의 .zshrc엔 그 초기화 구문이 빠져 있었던 거죠.
급한 대로는 node 경로를 직접 PATH에 끼워 넣어 해결했습니다.
PATH="$HOME/.nvm/versions/node/v24.17.0/bin:$PATH" \
JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" \
./gradlew assembleRelease
근본적인 해결은 뒤에서 .zshrc에 정리합니다.
Step 3. "No matching variant... No variants exist" — 진짜 함정
이제 진짜 골치 아픈 에러가 나왔습니다. 네이티브 모듈마다 이런 메시지가 줄줄이 떴습니다.
> Could not resolve project :react-native-screens.
> No matching variant of project :react-native-screens was found.
... No variants exist.
react-native-gesture-handler, react-native-safe-area-context, react-native-webview... 거의 모든 RN 라이브러리에서 "variant가 하나도 없다"고 합니다. 마치 라이브러리들이 빈 껍데기 프로젝트로 인식되는 모양새였죠.
첫 번째 의심: node_modules가 깨졌나?
복사해 온 node_modules가 불완전한가 싶어서, 먼저 package-lock.json 기준으로 깨끗하게 다시 받았습니다.
npm ci
npm ci는 기존 node_modules를 지우고 lock 파일 그대로 설치하기 때문에 "복사하다 일부 누락됐을지 모를" 상황에 딱 맞습니다. 그런데... 다시 빌드해도 같은 에러가 났습니다. 범인은 node_modules가 아니었던 겁니다.
진짜 원인: 복사돼 온 stale 빌드 캐시
핵심 단서는 android/ 폴더가 Git에 추적되지 않는다는 점이었습니다. .gitignore에 /android가 들어 있었거든요.
git ls-files android/ | wc -l # → 0
즉 android/는 expo prebuild로 만들어지는 순수 생성물이고, 그 안에는 이전 노트북에서 만들어진 빌드 캐시 폴더까지 함께 복사돼 온 상태였습니다.
ls -d android/.gradle android/build android/app/build
# 셋 다 존재 → 이전 환경의 절대 경로가 박제된 캐시
이 캐시 안에는 이전 노트북의 절대 경로가 그대로 들어 있습니다. 새 노트북에선 경로가 안 맞으니 Gradle이 라이브러리 프로젝트를 제대로 구성하지 못하고 "variant가 없다"고 뱉은 것이었죠.
여기서 한 가지 함정이 더 있습니다. ./gradlew clean으로는 .gradle 캐시가 안 지워집니다. clean은 build/ 산출물만 정리할 뿐이거든요. 그래서 직접 지웠습니다.
rm -rf android/.gradle android/build android/app/build android/.cxx
그리고 다시 빌드.
cd android && ./gradlew assembleRelease
BUILD SUCCESSFUL in 3m 13s
드디어 성공했습니다. 빌드된 APK는 여기에 떨어집니다.
android/app/build/outputs/apk/release/app-release.apk
이어서 USB로 연결된 기기에 설치했습니다.
adb install -r app/build/outputs/apk/release/app-release.apk
# → Success
✅ 교훈: 프로젝트를 복사해서 옮길 때
android/.gradle,android/build,android/app/build는 가져오지 마세요. 어차피 생성물이고, 오히려 이런 식으로 발목을 잡습니다. 깔끔하게 하려면npx expo prebuild --clean으로 android 폴더 자체를 새로 만드는 방법도 있습니다(이 경우 keystore와 서명 설정은 다시 넣어줘야 합니다).
Step 4. 설치는 됐는데 앱 목록에 안 보인다
Success가 떴으니 끝났겠거니 했는데, 폰의 앱 서랍을 아무리 뒤져도 앱 아이콘이 안 보였습니다. 분명 설치는 됐는데 말이죠.
먼저 정말 설치가 됐는지부터 확인했습니다.
adb shell pm list packages | grep chainplay
# → package:com.backdev.chainplay (설치돼 있음)
설치는 분명히 됐습니다. 그럼 런처에서 실행할 메인 액티비티가 등록이 안 됐나 싶어 확인했더니, 이상한 결과가 나왔습니다.
adb shell cmd package resolve-activity --brief \
-c android.intent.category.LAUNCHER com.backdev.chainplay
# → No activity found
그런데 dumpsys로 자세히 보니 MainActivity엔 MAIN + LAUNCHER 인텐트 필터가 멀쩡히 등록돼 있었습니다. 등록은 됐는데 런처에서 못 찾는다? 결정적 단서는 이 줄이었습니다.
adb shell dumpsys package com.backdev.chainplay | grep -i enabled
# ... enabled=3 ...
# lastDisabledCaller: com.lge.launcher3
enabled=3은 안드로이드에서 DISABLED_USER, 즉 "사용자에 의해 비활성화됨" 상태를 뜻합니다. 그리고 lastDisabledCaller가 com.lge.launcher3 — LG 런처가 이 앱을 비활성화시켜 둔 상태였습니다. 아마 예전에 구버전 앱을 "사용 안 함" 처리했던 게 재설치 후에도 그대로 남아 있었던 것 같습니다.
해결은 간단합니다. 앱을 다시 활성화해 주면 됩니다.
adb shell pm enable com.backdev.chainplay
# → new state: enabled
이러고 나니 런처 액티비티도 정상으로 잡히고, 앱 서랍에도 아이콘이 떴습니다. monkey로 바로 실행도 확인했고요.
adb shell monkey -p com.backdev.chainplay \
-c android.intent.category.LAUNCHER 1
참고로 안드로이드의
enabled상태 값은 이렇게 나뉩니다.
값 의미 0 DEFAULT 1 ENABLED 2 DISABLED 3 DISABLED_USER (사용자/런처가 비활성화) 4 DISABLED_UNTIL_USED
한 번 더 안 겪으려면: .zshrc에 환경 박아두기
매번 JAVA_HOME=..., PATH=...를 손으로 붙이는 건 고역입니다. 새 노트북에 정착할 거라면 셸 설정에 한 번 넣어두는 게 깔끔합니다. ~/.zshrc에 이렇게 추가했습니다.
# --- Android / React Native 빌드 환경 ---
export ANDROID_HOME="$HOME/Library/Android/sdk"
export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"
# Gradle용 JDK (Android Studio 내장 JBR)
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
# nvm (node를 PATH에 올림 — Gradle autolinking이 node 필요)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
새 터미널을 열고 확인해 보면(또는 source ~/.zshrc):
node -v # v24.17.0
which adb # .../platform-tools/adb
java -version # openjdk 21
이제 빌드는 환경 변수 없이 이 한 줄로 끝납니다.
cd android && ./gradlew assembleRelease && \
adb install -r app/build/outputs/apk/release/app-release.apk
자주 쓴 진단 명령어 모음
이번에 트러블슈팅하면서 유용했던 adb/Gradle 명령들을 정리해 둡니다.
| 목적 | 명령 |
|---|---|
| 연결 기기 확인 | adb devices |
| 설치된 패키지 검색 | adb shell pm list packages | grep <키워드> |
| 설치 경로 확인 | adb shell pm path <패키지명> |
| 런처 액티비티 조회 | adb shell cmd package resolve-activity --brief -c android.intent.category.LAUNCHER <패키지명> |
| enabled/상태 확인 | adb shell dumpsys package <패키지명> | grep -i enabled |
| 앱 활성화 | adb shell pm enable <패키지명> |
| 앱 실행 | adb shell monkey -p <패키지명> -c android.intent.category.LAUNCHER 1 |
| Gradle 캐시 완전 삭제 | rm -rf android/.gradle android/build android/app/build android/.cxx |
정리 — 핵심 흐름 한눈에
새 노트북으로 RN/Expo 프로젝트를 옮겨 빌드하면서 배운 걸 순서대로 요약하면 이렇습니다.
- JDK는 Android Studio 내장 JBR로 충분하다 —
JAVA_HOME만 잡아주면 됨 - Gradle은 빌드 중
node를 직접 실행한다 — 셸 PATH에 node가 없으면 실패. nvm 쓰면.zshrc초기화 필수 No variants exist는 복사돼 온 stale 빌드 캐시가 범인일 수 있다 —android/.gradle·build·app/build를 통째로 지우면 해결.gradlew clean으론 부족함- 설치됐는데 앱이 안 보이면
enabled상태를 의심하라 —adb shell pm enable로 부활
결국 네 가지 문제 모두 "새 환경인데 옛 환경의 흔적이 남아 있어서" 생긴 일이었습니다. 프로젝트를 옮길 땐 생성물(빌드 캐시)과 환경 의존 설정(PATH, JAVA_HOME)을 새 환경 기준으로 다시 만든다는 원칙만 기억하면, 다음번엔 훨씬 덜 헤맬 수 있을 겁니다.
backtodev
40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.