728x90
반응형

프로젝트들에 모노레포를 적용하면서 했던 의사결정과 pnpm에 대한 이모저모

 

모노레포 적용이 필요했던 배경

  • 멀티 레포로 관리되던 상황이었어서 공통의 util / 디자인 컴포넌트를 사용하기에 생각보다 큰 리소스가 예상되었다
  • 비즈니스의 흐름을 이해하기 어렵다
    • 피쳐 성격상 a웹과 b웹에서 같이 작업이 이루어지는 경우가 있을 수 있다
    • 이때 pr등에서 확인하기가 어렵고 여러 ide 창을 켜놔야 하는 경우도 있었다

=> 모노레포를 적용하고 이를 통해 위의 상황을 개선해보자!!


그래서 선택지는 총 3개

  • nx
  • pnpm
  • yarn berry

기존의 프로젝트 들은 yarn classic을 쓰고 있었다


처음 시도했던 것은 nx, 그러나

  • nx는 꽤나 어려웠다
  • 고점은 높아보였지만 러닝커브가 쉽지 않아 보였다
  • 회사 스테이지상 학습을 위한 시간을 확보하기는 매우 어렵다 <- 개인의 희생을 강요할 수 없다

이제 선택지는 yarn berry와 pnpm만 남았다

어찌할 것인가

 

pnpm을 선택하기로 했다

  • pnpm을 다뤄봤던 팀원이 있었고
  • npm trends를 믿어보기로 했다

 

(이 당시 잘못 알고 있었던 것은 yarn berry는 node_modules가 존재하지 않아 rn에서 쓸 수 없을까 였었다. 

yarn berry에서도 hoisted를 통해 node_modules를 생성할 수 있다)

 

pnpm은 빠르다

왜 빠른가

  • 설치시 pnpm store에 해당 라이브러리 들을 설치한다
  • a 프로젝트에서 라이브러리를 설치시 store로 부터 하드링크를 연결한다 <- 이미 설치되어 있다면 재설치를 하지 않는다 
  • node_modules/.pnpm에 store에서 가져온 라이브러리들이 존재한다
  • 비슷한 개념이 심링크 이다
    • 심링크는 바로가기로 연결만 한다
    • 이 심링크를 사용하는 것은 아래에서 확인하자
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       ├── bar -> <store>
    │       └── qar -> ../../qar@2.0.0/node_modules/qar <- /node_modules/.pnpm/qar@2.0.0/node_modules를 심링크
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo -> <store>
    │       ├── bar -> ../../bar@1.0.0/node_modules/bar  <- /node_modules/.pnpm/bar@1.0.0/node_modules를 심링크
    │       └── qar -> ../../qar@2.0.0/node_modules/qar  <- /node_modules/.pnpm/qar@2.0.0/node_modules를 심링크
    └── qar@2.0.0
        └── node_modules
            └── qar -> <store>

 

 

1. 프로젝트에서 foo@1.0.0 라이브러리를 추가한다

2. node_modules/.pnpm에 foo@1.0.0이 하드링크로 추가된다 (이때 원본은 .pnpm의 store에 위치한다)

    * package를 import하는 방법을 정할 수 있는데 가장 권장되는 수단은 COW(CopyOnWrite) file system 이다

    * COW는 기본적으로 주소값 복사를 하고 변경이 필요한 경우 변경점만 별도로 디스크를 할당한다

3. foo@1.0.0의 내부서 의존하는 bar@1.0.0 / qar@2.0.0이 .pnpm에 하드링크로 추가된다 

4. foo@1.0.0의 내부 라이브러리들은 각 .pnpm에 설치된 라이브러리들에 심링크로 연결되어 있다 

5. foo@1.0.0은 프로젝트에서 직접 참조되므로 .pnpm의 외부로 이동된다. 이때 .pnpm을 심링크로 연결한다

 

COW(CopyOnWrite)

 

(npm은 캐싱 없이 매번 설치마다 node_modules에서 디스크 공간을 차지해야 한다)

 

 

그 외에 장점은

  • 유령 의존성을 없앤다
    • 패키지 별로 node_modules를 가지고 버전 명세가 명확하다
    • hoisting이 되지 않는 구조에서 package.json에 명세 없이 코드상에서 import를 하는 것은 실행이 되지 않는다
유령 의존성이란?

내 프로젝트에서 쓰고 있지만 package.json에는 없는 애


node_modules/
	lodash/
    a/package.json
    b/package.json/lodash



패키지 a의 코드에서
import lodash from 'lodash' <- 요 코드를 실행할 수 있다 (npm과 yarn classic에서는 실행가능)

=> 런타임에서 에러발생이 가능한 환경이 된다

 


rn에서 pnpm을 사용할 때 hoisting을 사용해야 한다

왜냐하면 rn의 하위 버전에서는 symlink가 동작하지 않고 메트로 + 기존 라이브러리에서 node_modules의 계층형 구조를 인식하지 못한다

빌드시 매번 node_modules에 라이브러리가 존재하지 않는다는 에러를 마주할 수 있다

아래와 같이 rn 하위버전 metro 설정에서 override를 해줘야 할 수 도 있다

metro.config.js

const defaultConfig = getDefaultConfig(projectRoot);
const {
  resolver: { sourceExts, assetExts },
} = defaultConfig;

function resolveFromProject(modulePath) {
  return path.dirname(require.resolve(`${modulePath}/package.json`, { paths: [projectRoot] }));
}

const reactDir = resolveFromProject('react');

const config = {
  resolver: {
    extraNodeModules: {
      react: reactDir,
    },
  },
};

module.exports = mergeConfig(defaultConfig, config);

 

but

 

store를 통한 의존성 설치시의 캐싱활용은 장점이 될 수 있다

유령의존성 제거 장점을 취하지 못한 것은 아쉬울 수 있다

ci/cd에서의 장점은 .pnpm store를 github에서 캐싱해둠으로써 매번 새로 설치하지 않아도 되는 장점을 이룰 수 있다

(아래는 캐싱 예시)

- name: Cache pnpm store
  uses: actions/cache@v3
  with:
    path: ~/.pnpm-store
    key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}

 


 

.npmrc에서

node-linker를 hoisted로 설정할지 옵션을 설정할 수 있다

 

모노레포 프로젝트들의 버전을 override해줄 수 있다

이때 각 프로젝트의 node_modules에 라이브러리들이 설치가 된다

patch는 npm 환경에서 node_modules 내부 라이브러리를 직접 바꾸고 싶은경우 사용되는 방식이다

pnpm에서는 아래와 같이 설정할 수 있다

package.json

"pnpm": {
    "overrides": {
      "a>react": "19.0.0",
      "react": "18.2.0",
    },
    "patchedDependencies": {
      "c": "patches/c.patch",
      "d": "patches/d.patch",
    }
  },

 

root/
 ├─ apps/
 │   ├─ web/
 │   └─ admin/
 ├─ packages/
 │   ├─ ui/      (Shared Component Library)
 │   ├─ utils/   (Shared Business Logic)
 │   └─ config/  (ESLint, TSconfig shared)
 └─ pnpm-workspace.yaml

 

* apps는 실제 서비스이다

* packages는 공유 라이브러리 또는 컴포넌트 들이다

* pnpm은 내부 패키지에 대해서는 store를 거이지 않는다.

* pnpm은 내부 패키지들을 softlink 처리한다

* packages를 수정하면 바로 반영된다

 


 

pnpm의 선택은 단순 속도 문제는 아니었고

프로젝트 구조, 개발 경험(DX), 라이브러리의 선택 기준등 

생각보다 많은 것이 엮인 결정이었다.

728x90
반응형

'Frontend > React' 카테고리의 다른 글

Redux persist와 migration  (0) 2025.11.27
[번역] React Atomic하게 바라보기  (0) 2024.05.19
React patterns 🤔  (0) 2023.06.06
React 공식문서 주요개념 살펴보기  (1) 2022.05.26
React what is JSX? (번역글) 🤔  (2) 2022.04.27

+ Recent posts