들어가기에 앞서.. 패키지 매니저란?
개발에서 외부 라이브러리, 프레임워크 및 도구를 관리하는 도구다.
의존성 관리, 버전 관리, 패키지 업데이트 등을 수행한다.
1. npm(Node Package Manager)
Node.js의 기본 패키지 매니저다. 가장 오래되었고 널리 쓰인다.
package.json 파일을 통해 의존성을 관리한다. node_modules 폴더에 중첩 구조로 의존성을 설치한다.
설치 속도가 yarn, pnpm에 비해 느리다. 이는 중첩 구조로 인한 중복 패키지 설치 때문이다.
중첩 구조로 설치한다는 것이 단점인데, 중첩 구조란?
각 패키지가 자신의 의존성을 자신의 node_modules 폴더 내에 설치하는 방식이다. 예를 들어,
프로젝트/
├── node_modules/
│ ├── A/
│ │ ├── node_modules/
│ │ │ └── C@1.0.0/
│ │ └── index.js
│ ├── B/
│ │ ├── node_modules/
│ │ │ └── C@2.0.0/
│ │ └── index.js
│ └── ...
└── package.json
- 패키지 A는 C의 1.0.0 버전을 사용
- 패키지 B는 C의 2.0.0 버전을 사용
- 두 버전의 C가 별도로 설치됨
npm3 이상부터는 호이스팅 기법을 활용해 가능한 많은 패키지를 루트 node_modules로 끌어올려 중복을 제거하려 노력했지만, 구조적인 한계가 존재하기에 Yarn이나 pnpm보다 비효율적이다.
이는 특히 대규모 프로젝트에서 설치 시간 차이가 크게 나타난다.
이러한 문제를 Yarn은 병렬 설치와 캐싱을 이용해 해결한다.
2. Yarn(Yet Another Resource Negotiator)
Facebook에서 npm의 단점을 보완하기 위해 개발했다.
Yarn은 npm과 달리 여러 패키지를 동시에 다운로드하고 설치한다.
병렬 설치
npm 방식(순차적)
패키지 A 다운로드 → 패키지 A 설치 →
패키지 B 다운로드 → 패키지 B 설치 →
... (순차적으로 계속)
Yarn 방식(병렬)
패키지 A, B, C, D, E... 동시에 다운로드 시작 →
다운로드 완료된 패키지부터 설치 작업 진행
최신 npm도 병렬 다운로드를 지원하지만, 구현 방식과 효율성 측면에서 Yarn과 차이가 있다.
캐싱 시스템
Yarn은 다운로드한 모든 패키지를 로컬 캐시에 저장하여 재사용한다.
이로 인해 오프라인 환경에서도 이전에 설치한 패키지를 사용할 수 있으며, CI/CD 환경에서 동일 패키지를 반복 설치할 때 시간을 절약한다.
~/.yarn/cache/
├── react-16.13.1.tgz
├── react-dom-16.13.1.tgz
├── lodash-4.17.20.tgz
└── ...
Yarn Berry(Yarn 2+)의 Plug'n'Play(PnP)
Yarn Berry에서 추가된 Plug'n'Play는 node_modules 폴더 없이 의존성을 관리한다.
[기존 node_modules 방식]
프로젝트/
├── node_modules/ # 수천 개의 작은 파일로 구성된 매우 큰 폴더
│ ├── react/
│ ├── lodash/
│ └── ...
└── package.json
[PnP 방식]
프로젝트/
├── .yarn/
│ ├── cache/ # 패키지 zip 파일들
│ └── unplugged/ # 특수한 경우 필요한 실제 파일
├── .pnp.cjs # 의존성 맵핑 정보를 담은 파일
└── package.json
- 모든 패키지는 zip 파일로 압축되어 yarn/cache 폴더에 저장된다.
- pnp.cjs 파일에 모든 패키지의 위치와 의존성 관계 정보를 매핑한다.
- Node.js는 이 매핑 정보를 활용해 zip 파일에서 직접 모듈을 로드한다.
node_modules 생성이 필요 없기 때문에 설치가 빠르고, 파일이 zip으로 압축되기 때문에 공간 효율적이다.
이 PnP 방식은 특히 CI/CD 환경이나 모노레포 구조에서 큰 성능 이점을 발휘한다.
CI/CD 환경에서의 PnP
- 설치 단계 생략 가능: yarn/cache와 pnp.cjs를 Git에 포함시키면 CI 환경에서 yarn install 단계를 완전 생략한다.
- 캐시 무효화 감소: 패키지 변경이 없으면 캐시 무효화가 발생하지 않아 CI 캐시 활용도가 높아진다.
모노레포 구조에서의 PnP
[node_modules 방식]
모노레포/
├── packages/
│ ├── app1/
│ │ ├── node_modules/ (크기: 200MB)
│ │ └── package.json
│ ├── app2/
│ │ ├── node_modules/ (크기: 180MB)
│ │ └── package.json
│ └── ... 48개의 추가 패키지
└── package.json
총 저장 공간: ~10GB
초기 설치 시간: 15-20분
[PnP 방식]
모노레포/
├── .yarn/
│ └── cache/ (크기: 500MB - 모든 패키지가 공유)
├── packages/
│ ├── app1/
│ │ └── package.json
│ ├── app2/
│ │ └── package.json
│ └── ... 48개의 추가 패키지
├── .pnp.cjs
└── package.json
총 저장 공간: ~600MB
초기 설치 시간: 2-3분
3. pnpm(Performant Node Package Manager)
npm과 Yarn의 대안으로 등장한 패키지 매니저다. 특히 디스크 공간 효율성과 의존성 관리 엄격성에 초점을 맞춘다.
콘텐츠 주소 지정 저장소
pnpm의 가장 혁신적인 기능으로, 모든 패키지를 전역 저장소에 한 번만 저장하고 프로젝트에서는 심볼릭 링크를 통해 이를 참조한다.
~/.pnpm-store/ # 글로벌 저장소 (모든 프로젝트가 공유)
└── v3/
└── files/
└── 00/
└── 많은 패키지 파일들...
프로젝트/
└── node_modules/
├── .pnpm/ # 실제 패키지들이 저장된 위치
│ ├── react@16.13.1/
│ ├── lodash@4.17.20/
│ └── ...
├── react -> .pnpm/react@16.13.1/node_modules/react # 심볼릭 링크
├── lodash -> .pnpm/lodash@4.17.20/node_modules/lodash # 심볼릭 링크
└── ...
디스크 공간이 절약되며, 이미 저장소에 있는 패키지는 다운로드 없이 링크만 생성되어 설치 속도가 빠르다.
# 500개 패키지를 포함한 모노레포 설치 시
npm: 약 2GB 디스크 공간 사용, 설치 시간 15분
yarn: 약 1.5GB 디스크 공간 사용, 설치 시간 10분
pnpm: 약 600MB 디스크 공간 사용, 설치 시간 5분
엄격한 의존성 관리
pnpm은 "유령 의존성" 문제 해결을 위해 엄격한 의존성 관리를 제공한다.
유령 의존성이란? - 직접 의존하지 않는 패키지를 사용할 수 있는 문제를 뜻한다.
// package.json에 express만 명시, axios는 명시하지 않음
// npm/yarn에서는 동작함 (express가 의존하는 패키지가 노출됨)
const axios = require('axios');
// pnpm에서는 에러 발생 (package.json에 명시된 패키지만 사용 가능)
// Error: Cannot find module 'axios'
node_modules/
├── .pnpm/
│ ├── express@4.17.1/
│ │ └── node_modules/
│ │ ├── express/
│ │ └── (express의 모든 의존성)
│ └── axios@0.21.1/
│ └── node_modules/
│ └── axios/
├── express -> .pnpm/express@4.17.1/node_modules/express
└── (package.json에 명시된 다른 패키지들)
pnpm의 구조에선 express가 사용하는 의존성들이 express/node_modules에 격리되어 있어, 직접 접근이 불가하다.
그런데, npm에서 문제가 되었던 중첩 구조가 다시 나타난 것으로 보인다. 성능 문제는 없을까?
pnpm의 구조는 npm의 중첩 구조와 상당히 다른 접근이다.
pnpm의 구조에서는 pnpm-store에 실제 파일이 한 번만 저장되므로 파일 중복이 없으며, 패키지마다의 node_modules는 성능 문제가 아닌 의도적인 보안/의존성 격리 설계다.
따라서 겉보기에는 중첩 구조로 보이지만, 실제로는 성능과 보안을 모두 개선한 우아한 접근법이다.
대부분의 경우, 특히 모노레포 구조에서는 되도록 pnpm을 사용하는 것이 효율적일 것이다.
'Frontend' 카테고리의 다른 글
[NextJS] 이미지 파일 관리, 최적화 (SVG, SVGO) (3) | 2025.02.09 |
---|---|
[React] 조건부 렌더링 방식 (얼리 리턴, 삼항 연산자, 논리 AND 연산자) (0) | 2025.01.10 |
[Axios] 중복된 토큰 재발급 요청을 하나로 줄이기 (Axios Interceptor) (2) | 2024.12.08 |
[React] useMemo 실제 활용해보기 (0) | 2024.11.28 |
[ESLint] ESLint가 vscode에서 적용되지 않을 때 (2) | 2024.11.26 |