문제 상황
: Unable to resolve module XX from ..
원인
-
pnpm은 strict node_modules구조를 가진다.
다시말해 pnpm 워크스페이스에서 의존성은 각 패키지에서 symlink로 관리하며, 각 패키지의 의존성은 루트 node_modules의 의존성을 함께 사용한다.
-
Expo의 Metro 번들러가 symlink를 기본적으로 follow하지 않는다.
Metro 번들러는 파일을 resolve할 때 실제 파일 시스템 경로 기반으로 탐색한다. → metro가 react를 찾으러가면 symlink를 만나는데, symlink의 실제 경로(
.pnpm/..)를 따라가면 그 위치를 기준으로 다시 의존성을 찾게된다. 그러면 .pnpm내부의 격리된 구조 안에서 다른 의존성을 찾지 못하는 연쇄 문제가 발생한다.
다양한 해결 방법
-
npm/yarn classic은 flat한 node_modules구조이기에 metro가 문제없이 탐색한다.
-
pnpm을 계속 사용한다면 .npmrc에
node-linker=hoisted를 설정해 flat하게 설치하도록 한다.- 이 방법을 사용할 경우 pnpm의 장점(strict node_modules, 유령 의존성 해결)이 희석된다.
-
.npmrc에
public-hoist-pattern로 문제가 되는 각 패키지를 추가한다.// .npmrc public-hoist-pattern[]=*expo* public-hoist-pattern[]=@expo/* public-hoist-pattern[]=*react-native* public-hoist-pattern[]=*react*public-hoist-pattern: 매칭되는 패키지만 루트 node_modules로 호이스팅된다.
- 찾지 못하는 패키지가 너무 많아서 모두 명시할 수 없다.
-
metro.config.js에서 워크스페이스 루트를 인식시키도록 한다.
const { getDefaultConfig } = require('expo/metro-config'); const path = require('path'); const projectRoot = __dirname; const monorepoRoot = path.resolve(projectRoot, '../..'); const config = getDefaultConfig(projectRoot); // 모노레포 루트의 node_modules도 탐색하도록 설정 config.watchFolders = [monorepoRoot]; config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'), ]; // 모노레포 내 다른 패키지에서 별도 react / react-native 인스턴스를 쓰지 않도록 config.resolver.disableHierarchicalLookup = true; module.exports = config;
최종 결정
.npmrc > node-linker=hoisting 추가
-
이유
react-lucide를 사용하는 각종 패키지에서 타입 오류 발생
- isolated모드에서는 .pnpm store에 @types/react@18과 @types/react@19가 공존 → @types/react@19쪽으로 resolve되면서 타입 불일치
- content-ui 등 패키지의 @types/react는 18인데, lucide-react가 export하는 타입은 @types/react@19의 ReactNode(bigint 포함)를 참조해서 호환이 안됨.
딥다이브
-
node-linker=hoisting을 설정하지 않을 경우(pnpm의 default상태인 strict node_modules 구조): 각 패키지는 루트node_modules/.pnpm을 symlink로 가진다.
모든 패키지는 symlink로 루트
node_modules/.pnpm에 있는 패키지를 가리킨다.예외 : react,react-dom. 루트에는 react@18이 설치되어있고, 이 app패키지에서는 react@19에 의존하기 때문.
-
루트 node_modules: 루트에서 설치한 패키지 저장 루트 node_modules/.pnpm: 모든 패키지의 실제 파일이 flat하게 저장되는 곳
이 두 가지를 둠으로써 유령 의존성을 해결한다.

-
그리고 루트의 node_modules의 패키지들은 루트의 node_modules/.pnpm에 있는 패키지를 symlink로 가리키고, node_modules/.pnpm에 있는 패키지들은 전역 저장소의 패키지를 hard link로 가리킨다.
-
.npmrc의 node-linker=hoisted를 설정할 경우, flat한 루트 node_modules
@types/react도 하나의 버전(18)만 설치가 된다.
근본적으로 해결할 수는 없을까?
node-linker=hoisted는 pnpm의 핵심 이유(strict depdencies, 유령 의존성 방지)를 포기하는 거라 npm/yarn classic과 다를 바 없어진다.
-
react 버전을 19로 통일
-
metro.config.js 설정 + pnpm overrides
// metro.config.js config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'), ]; config.watchFolders = [monorepoRoot];// package.json (root) { "pnpm": { "overrides": { "@types/react": "^18.0.0" // 또는 19로 통일 } } }→ app에서 문제 발생 가능성 O
- 버전을 19로 통일 & npmrc 제거
-
타입 문제
-
- 로컬에서는 잘된다 → node_modules 전부 제거하고 재설치하니 구조가 새롭게 만들어짐
- “.npmrc의 node-linker=hoisted제거하고 pnpm install을 해도 새로 구조가 만들어지지 않는다.”
- 원인: .npmrc의 node-linker=hoisted가 제거되며 pnpm의 strict node_modules 구조로 설치가 되었다. 기존에 app이 유령 의존성으로 @types/chrome으로 타입을 resolve하고 있었는데, 없어지면서 발생한 문제이다.
- app은 실제로 chrome을 사용하지 않지만, shrared패키지를 사용해서 발생한 문제이다. →