이 글에서 정리하는 내용
Expo 프로젝트에서 Unable to resolve module 오류가 발생했을 때, Metro 캐시를 지우기 전에 import 경로, 파일명 대소문자, 패키지 설치 상태, alias 설정을 순서대로 확인하는 기준을 정리합니다.
- 오류 메시지에서 먼저 봐야 할 부분
- import 경로가 틀어진 경우
- 패키지를 못 찾는 경우
- alias와 Metro 설정을 확인해야 하는 경우
- 캐시 초기화는 언제 해야 하는가
- 다시 같은 오류를 줄이기 위한 기준
오류 메시지에서 먼저 봐야 할 부분

Expo에서 앱을 실행하다가 Unable to resolve module 오류가 나오면, 가장 먼저 캐시부터 의심하기 쉽습니다. 실제로 이전 변환 결과가 남아 있어서 문제가 이어지는 경우도 있습니다. 다만 이 오류의 출발점은 보통 더 단순합니다. import 문에 적힌 경로가 현재 파일 구조와 맞지 않거나, 설치되지 않은 패키지를 코드에서 불러오고 있을 때 먼저 발생합니다.
Metro는 Expo 개발 서버에서 JavaScript와 TypeScript 파일을 읽고, import 문을 따라 필요한 모듈을 찾아 번들로 묶습니다. 이때 import에 적힌 이름을 실제 파일이나 패키지로 연결하지 못하면 Unable to resolve module 오류가 납니다. 그래서 이 오류는 “앱 화면이 왜 안 뜨지?”보다 “Metro가 어떤 대상을 못 찾았지?”로 접근하는 편이 빠릅니다.
오류 메시지에 ./components/Button처럼 상대 경로가 보이면 로컬 파일 import 문제일 가능성이 높습니다. react-native-svg처럼 패키지명이 그대로 보이면 설치 상태나 Expo SDK 호환 버전을 먼저 봐야 합니다. @/components/Button처럼 별칭이 보이면 TypeScript 설정과 Metro가 같은 기준으로 경로를 해석하고 있는지 확인해야 합니다.
오류 메시지에서 나눌 기준
Unable to resolve "../components/UserCard" from "app/index.tsx"
Unable to resolve "react-native-svg" from "components/Icon.tsx"
Unable to resolve "@/components/Button" from "app/profile.tsx"
위 세 줄은 모두 비슷한 오류처럼 보이지만 확인할 파일이 다릅니다. 첫 번째는 현재 파일 기준 상대 경로를 봐야 하고, 두 번째는 package.json과 설치 상태를 봐야 하며, 세 번째는 alias 설정을 확인해야 합니다. 이 구분 없이 npx expo start --clear만 반복하면 실제로 틀어진 경로를 계속 캐시 문제로 오해하게 됩니다.
Expo Router의 route not found 오류와도 분리해야 합니다. route 오류는 URL과 app 폴더의 라우트 파일 구조가 맞지 않을 때 주로 발생합니다. 반면 Unable to resolve module은 화면으로 이동하기 전, Metro가 소스 코드의 import 의존성을 해석하다가 멈춘 상태입니다. 라우터 문제가 아니라 번들링 단계에서 import 대상을 못 찾은 문제로 보는 것이 맞습니다.
import 경로가 틀어진 경우
로컬 파일을 불러오는 import라면 실제 폴더 위치부터 확인해야 합니다. Expo Router 프로젝트에서는 app 폴더와 components, constants, assets 폴더를 나눠 쓰는 경우가 많습니다. 컴포넌트를 한 번 이동하거나 폴더를 정리한 뒤에는 이전 import 경로가 그대로 남아 Metro가 예전 위치에서 파일을 찾다가 실패할 수 있습니다.
예를 들어 아래와 같은 구조에서 app/index.tsx가 components/UserCard.tsx를 불러온다고 가정하겠습니다.
app/
index.tsx
components/
UserCard.tsx
constants/
theme.ts
app/index.tsx 기준으로 components는 같은 폴더 안에 있는 하위 폴더가 아니라 프로젝트 루트의 형제 폴더입니다. 현재 파일 기준 상대 경로를 잘못 계산하면 Metro는 존재하지 않는 app/components/UserCard를 찾게 됩니다.
import UserCard from './components/UserCard';
위 코드는 app/components/UserCard를 찾는 형태입니다. 실제 파일이 components/UserCard.tsx에 있다면 한 단계 위로 나간 뒤 접근해야 합니다.
import UserCard from '../components/UserCard';
경로를 확인할 때는 프로젝트 루트가 아니라 import를 작성한 파일을 기준점으로 잡아야 합니다. 특히 app/(tabs)/profile.tsx처럼 그룹 폴더가 들어가면 화면 URL에는 (tabs)가 보이지 않아도 파일 시스템 경로에는 폴더가 존재합니다. URL에 안 보이는 폴더라고 해서 상대 경로 계산에서 빠지는 것은 아닙니다.
파일명 대소문자도 같이 확인하기
파일이 있는데도 못 찾는 것처럼 보이면 파일명 대소문자를 확인해야 합니다. UserCard.tsx 파일을 만들고 import에서는 userCard로 적는 식의 차이는 빠르게 훑으면 놓치기 쉽습니다.
import UserCard from '../components/userCard';
Windows 환경에서는 대소문자 차이가 늦게 드러날 수 있습니다. 하지만 Git, macOS, Linux, CI 빌드, EAS Build 환경으로 넘어가면 같은 코드가 갑자기 깨질 수 있습니다. 로컬에서 한 번 실행됐다는 이유만으로 대소문자 문제를 제외하면 안 됩니다.
확장자는 보통 생략해서 import할 수 있지만, 파일명이 바뀐 상태에서 예전 이름이 남아 있거나 index.ts로 묶어 내보내는 구조를 바꾸면 경로가 달라집니다. 컴포넌트를 이동했거나 폴더 이름을 바꾼 직후라면 에디터의 자동 import가 예전 경로를 물고 있지 않은지도 같이 확인해야 합니다.
패키지를 못 찾는 경우
오류 메시지에 상대 경로가 아니라 패키지명이 그대로 보이면 확인 지점이 달라집니다. 예를 들어 react-native-svg, expo-image-picker, @react-navigation/native 같은 이름이 나온다면 파일 위치보다 설치 상태를 먼저 보는 것이 맞습니다.
import Svg, { Path } from 'react-native-svg';
코드만 보면 문제가 없어 보여도 프로젝트에 react-native-svg가 설치되어 있지 않으면 Metro는 해당 패키지를 찾을 수 없습니다. 먼저 package.json의 dependencies 또는 devDependencies에 해당 패키지가 있는지 확인합니다.
{
"dependencies": {
"expo": "...",
"react": "...",
"react-native": "...",
"react-native-svg": "..."
}
}
Expo 프로젝트에서는 React Native 관련 패키지를 설치할 때 단순히 최신 버전을 직접 고르는 것보다 현재 Expo SDK와 맞는 버전을 설치하는 방식이 안정적입니다. 보통은 npx expo install 패키지명 형식으로 설치하면 SDK 호환 버전에 맞춰 설치됩니다.
npx expo install react-native-svg
이미 설치했는데도 같은 오류가 난다면 개발 서버가 이전 상태를 보고 있을 수 있습니다. 이때는 서버를 끄고 다시 실행합니다. 그래도 해결되지 않으면 node_modules와 lockfile 상태를 확인합니다. npm, yarn, pnpm을 섞어 사용한 프로젝트에서는 package-lock.json, yarn.lock, pnpm-lock.yaml이 함께 남아 의존성 상태가 애매해질 수 있습니다.
재설치가 필요한 상황
rm -rf node_modules
npm install
npx expo start
Windows PowerShell에서는 아래처럼 삭제할 수 있습니다.
Remove-Item -Recurse -Force .node_modules
npm install
npx expo start
의존성을 다시 설치할 때는 사용 중인 패키지 매니저를 하나로 정리하는 것이 먼저입니다. npm을 쓰는 프로젝트라면 npm 기준으로, pnpm을 쓰는 프로젝트라면 pnpm 기준으로 맞춰야 합니다. lockfile이 섞인 상태에서 삭제와 설치를 반복하면 같은 패키지라도 해석 결과가 달라질 수 있습니다.
| 오류 메시지에 보이는 형태 | 먼저 확인할 위치 |
|---|---|
../components/UserCard | 현재 파일 기준 상대 경로, 파일명, 폴더 위치 |
react-native-svg | package.json, 설치 여부, Expo SDK 호환 버전 |
@/components/Button | tsconfig.json, alias 설정, Metro 재시작 여부 |
alias와 Metro 설정을 확인해야 하는 경우
프로젝트가 커지면 ../../../components/Button처럼 상대 경로가 길어집니다. 이때 src/components/Button 또는 @/components/Button 같은 alias import를 쓰고 싶어집니다. alias는 경로를 짧게 만들지만, 설정이 애매하면 Metro가 실제 위치를 해석하지 못하는 원인이 됩니다.
Expo의 TypeScript 설정에서는 baseUrl을 잡아 절대 import를 사용할 수 있습니다. 루트 기준 import를 쓰려면 tsconfig.json에서 기준 위치를 명확히 둡니다.
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"baseUrl": "./"
}
}
이 설정을 사용하면 프로젝트 루트를 기준으로 경로를 작성할 수 있습니다. 예를 들어 src/components/Button.tsx가 있다면 아래처럼 import할 수 있습니다.
import Button from 'src/components/Button';
@ 별칭을 별도로 쓰고 있다면 paths 설정까지 확인해야 합니다. 다만 alias 설정은 프로젝트의 Expo SDK, TypeScript 설정, 기존 Babel 설정, Metro 설정에 따라 다르게 보일 수 있습니다. 예전 방식으로 babel-plugin-module-resolver를 추가해 둔 프로젝트라면 해당 플러그인이 실제로 설치되어 있는지도 확인해야 합니다. 플러그인 이름만 babel.config.js에 남아 있고 패키지는 빠져 있으면, import 대상이 아니라 Babel 플러그인 자체를 찾지 못하는 오류가 날 수 있습니다.
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
설정을 바꾼 뒤에는 Expo CLI를 다시 시작해야 합니다. tsconfig.json을 고쳤는데도 개발 서버를 계속 켜 둔 상태라면, 에디터에서는 경로가 맞는 것처럼 보여도 Metro가 이전 설정으로 실행 중일 수 있습니다.
metro.config.js는 무조건 건드릴 파일이 아닙니다. CSS, monorepo, 특정 모듈 alias, 커스텀 resolver처럼 Metro의 기본 해석 방식을 바꿔야 할 때 사용합니다. 경로 하나가 틀어진 상태에서 Metro 설정부터 수정하면 원래 간단했던 오류가 더 복잡해집니다. 먼저 import 경로와 tsconfig.json을 확인하고, 그래도 alias가 해석되지 않을 때 Metro 설정을 보는 순서가 낫습니다.
캐시 초기화는 언제 해야 하는가

캐시 초기화는 필요한 작업입니다. 다만 모든 Unable to resolve module 오류의 첫 번째 해결책은 아닙니다. 파일 경로가 틀렸거나 패키지가 설치되어 있지 않다면 캐시를 지워도 같은 줄에서 다시 멈춥니다. 캐시 초기화는 “코드와 설치 상태는 맞는데 이전 상태가 남아 있는 것 같다”는 판단이 섰을 때 쓰는 단계로 두는 것이 좋습니다.
대표적으로 파일명을 고쳤는데도 예전 이름을 계속 찾는 경우, 패키지를 설치하고 서버를 다시 켰는데도 같은 오류가 남는 경우, Babel 또는 Metro 설정을 바꾼 뒤 이전 변환 결과가 남아 있는 듯한 경우에 캐시 초기화를 시도할 수 있습니다.
npx expo start --clear
이 명령은 Expo 개발 서버를 실행하면서 Metro 번들러 캐시를 지우는 방식입니다. 캐시를 지운 뒤 첫 실행은 다시 변환해야 하므로 평소보다 조금 느릴 수 있습니다. 이때 새 오류가 나온다면 캐시가 원인이 아니라, 캐시가 사라지면서 실제 문제가 더 정확히 드러난 것일 수 있습니다.
캐시 초기화 전에 확인할 체크 순서
1. 오류 메시지에 적힌 모듈 이름 확인
2. 상대 경로인지 패키지명인지 alias인지 구분
3. 파일 위치와 파일명 대소문자 확인
4. package.json과 설치 상태 확인
5. tsconfig.json 또는 metro.config.js 변경 여부 확인
6. 마지막으로 npx expo start --clear 실행
이 순서로 보면 같은 명령어를 반복하는 시간을 줄일 수 있습니다. 특히 Expo Router 프로젝트에서는 파일 이동이 잦습니다. 화면 파일, 레이아웃 파일, 공통 컴포넌트 폴더를 정리하다 보면 import 경로가 쉽게 남습니다. 오류가 난 줄을 먼저 열고, 그 줄의 import가 지금 폴더 구조와 맞는지 확인하는 습관이 가장 빠릅니다.
그래도 해결되지 않는다면 프로젝트 구조가 일반적인 단일 Expo 앱인지, monorepo인지, 직접 수정한 Metro 설정이 있는지 확인해야 합니다. 이 단계부터는 단순 캐시 문제가 아니라 프로젝트 설정 문제로 넘어갑니다. 최근에 metro.config.js, babel.config.js, tsconfig.json, package.json 중 어떤 파일을 바꿨는지 역순으로 보면 원인을 좁히기 쉽습니다.
다시 같은 오류를 줄이기 위한 기준
Unable to resolve module 오류는 메시지가 비슷해서 전부 캐시 문제처럼 보일 수 있습니다. 하지만 Metro가 실제로 하는 일은 import 문을 따라 파일과 패키지를 찾는 것입니다. 그래서 해결 순서도 캐시 삭제가 아니라 import 대상 확인에서 시작해야 합니다.
로컬 파일 import라면 현재 파일 기준의 상대 경로, 폴더 위치, 파일명 대소문자를 봅니다. 패키지 import라면 설치 여부와 Expo SDK에 맞는 설치 방식을 확인합니다. alias import라면 tsconfig.json과 개발 서버 재시작 여부를 확인합니다. 이 세 가지가 맞는데도 이전 상태가 남아 있는 것처럼 보일 때 npx expo start --clear를 실행하면 됩니다.
다음에 같은 오류가 나오면 오류 메시지의 모듈 이름을 먼저 분류해두는 것이 좋습니다. 상대 경로, 패키지명, alias 중 어디에 속하는지만 나눠도 확인할 파일이 좁아집니다. 캐시 초기화는 그 다음 단계입니다. 이 기준을 잡아두면 Expo Router의 라우트 문제와 Metro의 모듈 해석 문제도 섞이지 않습니다.