문제 상황

: Unable to resolve module XX from ..

image.png

원인

  1. pnpm은 strict node_modules구조를 가진다.

    다시말해 pnpm 워크스페이스에서 의존성은 각 패키지에서 symlink로 관리하며, 각 패키지의 의존성은 루트 node_modules의 의존성을 함께 사용한다.

  2. Expo의 Metro 번들러가 symlink를 기본적으로 follow하지 않는다.

    Metro 번들러는 파일을 resolve할 때 실제 파일 시스템 경로 기반으로 탐색한다. → metro가 react를 찾으러가면 symlink를 만나는데, symlink의 실제 경로(.pnpm/..)를 따라가면 그 위치를 기준으로 다시 의존성을 찾게된다. 그러면 .pnpm내부의 격리된 구조 안에서 다른 의존성을 찾지 못하는 연쇄 문제가 발생한다.

다양한 해결 방법

  1. npm/yarn classic은 flat한 node_modules구조이기에 metro가 문제없이 탐색한다.

  2. pnpm을 계속 사용한다면 .npmrc에 node-linker=hoisted를 설정해 flat하게 설치하도록 한다.

  3. .npmrc에 public-hoist-pattern로 문제가 되는 각 패키지를 추가한다.

    // .npmrc
    public-hoist-pattern[]=*expo*
    public-hoist-pattern[]=@expo/*
    public-hoist-pattern[]=*react-native*
    public-hoist-pattern[]=*react*
    
  4. 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 추가

딥다이브

  1. node-linker=hoisting을 설정하지 않을 경우(pnpm의 default상태인 strict node_modules 구조): 각 패키지는 루트 node_modules/.pnpm을 symlink로 가진다.

    image.png

    모든 패키지는 symlink로 루트 node_modules/.pnpm에 있는 패키지를 가리킨다.

    예외 : react,react-dom. 루트에는 react@18이 설치되어있고, 이 app패키지에서는 react@19에 의존하기 때문.