테마
pnpm 워크스페이스
pnpm의 하드링크/심볼릭 링크 기반 의존성 관리 전략과 비-평탄 node_modules 구조를 이해하고, 마이크로프론트엔드 환경에서 pnpm이 선호되는 이유를 파악한다.
학습 목표
- pnpm이 추구하는 철학(빠르고 효율적인 패키지 관리)을 이해한다.
- 비-평탄(non-flat)
node_modules구조의 동작 원리를 파악한다. - 하드링크와 심볼릭 링크 전략이 디스크를 절약하는 메커니즘을 이해한다.
- 엄격한 의존성 관리로 Phantom Dependency를 방지하는 방식을 이해한다.
pnpm-workspace.yaml설정과 워크스페이스 명령어를 익힌다.- 마이크로프론트엔드에서 pnpm이 선호되는 이유를 정리한다.
1. pnpm의 철학: 빠르고 효율적
pnpm(Performant npm)은 기존 패키지 매니저에 대한 불만에서 탄생했다. 핵심 키워드는 두 가지이다.
- 빠르고(Fast): 필요한 패키지만 식별하여 설치하므로 속도가 빠르다
- 효율적(Efficient): 글로벌 스토어에 패키지를 한 번만 저장하고 하드링크로 연결하므로 디스크를 절약한다
추가로 다음 특성을 가진다.
- 모노레포 완벽 지원:
pnpm-workspace.yaml로 워크스페이스 구성 - 비-평탄 node_modules:
node_modules의 최상위에는 직접 의존성만 노출 - 엄격한 의존성 관리: Phantom Dependency 원천 차단
1.1 pnpm 설치
bash
# npm을 이용한 설치
npm install -g pnpm@8.10.0
# corepack을 이용한 설치 (Node.js 16.13+)
corepack enable
corepack use pnpm@8.10.0
# 설치 확인
pnpm -vcorepack use 명령을 사용하면 package.json에 packageManager 필드가 추가되어, 프로젝트에서 다른 패키지 매니저를 사용하려 할 때 경고를 표시한다.
2. 비-평탄(Non-flat) node_modules 구조
npm과 Yarn Classic은 모든 의존성을 node_modules 최상위에 평탄하게 배치한다. pnpm은 이와 다르게 직접 의존성만 최상위에 노출하고, 나머지는 .pnpm/ 가상 스토어(Virtual Store) 내부에 격리한다.
2.1 구조 비교
[npm / Yarn Classic - 평탄 구조] [pnpm - 비-평탄 구조]
node_modules/ node_modules/
axios/ <-- 직접 의존성 .pnpm/
follow-redirects/ <-- axios의 의존성 axios@1.6.0/
form-data/ <-- axios의 의존성 node_modules/
proxy-from-env/ <-- axios의 의존성 axios/ <-- 하드링크
follow-redirects/ <-- 심볼릭 링크
follow-redirects@1.15.0/
node_modules/
follow-redirects/ <-- 하드링크
axios -> .pnpm/axios@1.6.0/.../axiosnpm/Yarn Classic: follow-redirects가 최상위에 노출되어 직접 require('follow-redirects') 가능 (Phantom Dependency)
pnpm: 최상위에는 axios 심볼릭 링크만 존재. follow-redirects는 .pnpm/ 내부에 격리되어 직접 접근 불가
3. 하드링크/심볼릭 링크 전략으로 디스크 절약
3.1 글로벌 스토어 (Content-Addressable Store)
pnpm은 시스템 전체에서 단 하나의 글로벌 스토어를 사용한다. 모든 패키지 파일은 이 스토어에 한 번만 저장되고, 각 프로젝트에서는 하드링크로 연결한다.
bash
# 글로벌 스토어 위치 확인
pnpm config get store-dir
# 예: ~/.pnpm-store
# 글로벌 스토어 위치 변경
pnpm config set store-dir /custom/path/.pnpm-store3.2 하드링크와 심볼릭 링크
| 링크 종류 | 설명 | pnpm에서의 용도 |
|---|---|---|
| 하드링크(Hard Link) | 같은 inode를 공유하는 파일. 원본과 동일한 데이터를 가리키되 디스크 공간을 추가로 사용하지 않음 | 글로벌 스토어의 파일 -> .pnpm/ 가상 스토어의 파일 |
| 심볼릭 링크(Symbolic Link) | 다른 파일/디렉터리를 가리키는 포인터 파일 | node_modules/axios -> .pnpm/axios@1.6.0/... |
3.3 설치 과정
pnpm의 설치는 세 단계로 이루어진다.
1단계 - 해석(Resolution): 필요한 패키지와 정확한 버전을 결정 2단계 - 페치(Fetching): 글로벌 스토어에 없는 패키지만 레지스트리에서 다운로드 3단계 - 링킹(Linking): 하드링크로 가상 스토어를 구성하고, 심볼릭 링크로 node_modules 디렉터리 구조를 완성
3.4 디스크 절약 효과
프로젝트 A: axios@1.6.0 사용
프로젝트 B: axios@1.6.0 사용
프로젝트 C: axios@1.6.0 사용
[npm/Yarn Classic]
각 프로젝트에 axios 복사 -> 디스크 3배 사용
[pnpm]
글로벌 스토어에 axios 1회 저장
3개 프로젝트에서 하드링크 -> 디스크 1배 사용같은 버전의 패키지가 10개 프로젝트에서 사용되어도 디스크에는 1개분의 용량만 차지한다. 대규모 모노레포나 여러 프로젝트를 다루는 개발 환경에서 효과가 극대화된다.
4. 엄격한 의존성 관리 (Phantom Dependency 방지)
pnpm의 비-평탄 구조는 package.json에 선언하지 않은 패키지에 대한 접근을 물리적으로 차단한다.
4.1 동작 원리
packages/b/
package.json # dependencies: { "axios": "^1.6.0" }
node_modules/
axios -> ../../node_modules/.pnpm/axios@1.6.0/.../axios # 심볼릭 링크
packages/a/
package.json # dependencies: { "b": "workspace:*" }
node_modules/
b -> ../../packages/b # 워크스페이스 심볼릭 링크
(axios가 여기에 없음 -> require('axios') 시 에러!)packages/a/의 node_modules/에는 b만 존재한다. axios는 b가 사용하는 의존성이지만, a의 node_modules에 호이스팅되지 않으므로 a에서 직접 접근할 수 없다.
4.2 Phantom Dependency 발생 예시 비교
javascript
// packages/a/index.js
// npm/Yarn Classic: 동작함 (위험!)
// pnpm: MODULE_NOT_FOUND 에러 (안전!)
const axios = require('axios'); // a의 package.json에 axios 선언 안 됨pnpm에서 이 코드를 실행하면 즉시 에러가 발생하므로, 개발자는 의존성을 올바른 위치에 선언하도록 강제된다. 이는 장기적으로 프로젝트의 안정성과 유지보수성을 크게 향상시킨다.
5. pnpm-workspace.yaml 설정
pnpm은 npm이나 Yarn과 달리, 워크스페이스 선언을 package.json이 아닌 별도의 pnpm-workspace.yaml 파일에서 한다.
5.1 기본 설정
yaml
# pnpm-workspace.yaml
packages:
- 'packages/*'5.2 다양한 패턴
yaml
# pnpm-workspace.yaml
packages:
- 'packages/*' # packages 하위 모든 디렉터리
- 'apps/*' # apps 하위 모든 디렉터리
- 'shared/utils' # 특정 디렉터리 지정
- '!**/test/**' # test 디렉터리는 제외5.3 전체 프로젝트 구조
monorepo-root/
package.json # 루트 패키지
pnpm-workspace.yaml # 워크스페이스 정의
pnpm-lock.yaml # 통합 lock 파일
.npmrc # pnpm 설정 (선택)
node_modules/
.pnpm/ # 가상 스토어
a -> ../packages/a # 워크스페이스 심볼릭 링크
b -> ../packages/b
packages/
a/
package.json
node_modules/
b -> ../../b # 워크스페이스 의존성
index.js
b/
package.json
node_modules/
axios -> .../.pnpm/axios@1.6.0/...
index.js6. 워크스페이스 명령어
6.1 의존성 관리
bash
# 전체 설치
pnpm install
# 특정 워크스페이스에 의존성 추가
pnpm --filter a add axios
pnpm --filter b add lodash
# 특정 워크스페이스에서 의존성 제거
pnpm --filter a remove axios
# 내부 워크스페이스를 의존성으로 추가
# a의 package.json에 "b": "workspace:*" 추가 후
pnpm install6.2 --filter의 강력한 기능
pnpm의 --filter 플래그는 매우 유연하다.
bash
# 이름으로 필터
pnpm --filter a run build
# 글로브 패턴
pnpm --filter "packages/*" run build
# 변경된 패키지만 (Git 기준)
pnpm --filter "...[origin/main]" run build
# 특정 패키지와 그 의존성 모두
pnpm --filter "a..." run build
# 특정 패키지에 의존하는 모든 패키지
pnpm --filter "...^a" run build6.3 스크립트 실행
bash
# 특정 워크스페이스의 스크립트 실행
pnpm --filter a start
# 모든 워크스페이스에서 실행
pnpm -r run build
# 워크스페이스 루트까지 포함
pnpm -r --include-workspace-root run start
# 의존성 목록 확인
pnpm -r list6.4 유용한 설정 (.npmrc)
ini
# .npmrc
# 호이스팅 패턴 설정 (특정 패키지만 호이스팅 허용)
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
# 엄격 모드 (기본값: true)
strict-peer-dependencies=true
# 심볼릭 링크 대신 하드링크 사용
symlink=true7. 실습: pnpm 워크스페이스 구성
7.1 프로젝트 초기화
bash
# 루트 프로젝트 생성
mkdir pnpm-workspaces-example && cd pnpm-workspaces-example
pnpm init
# corepack으로 패키지 매니저 고정
corepack use pnpm@8.10.0
# 워크스페이스 정의
# pnpm-workspace.yaml 생성yaml
# pnpm-workspace.yaml
packages:
- 'packages/*'7.2 패키지 생성 및 의존성 설정
bash
# 패키지 디렉터리 생성
mkdir -p packages/a packages/b
# 각 패키지 초기화
cd packages/a && pnpm init && cd ../..
cd packages/b && pnpm init && cd ../..
# b에 axios 추가
pnpm --filter b add axios
# a에 워크스페이스 b를 의존성으로 추가
# packages/a/package.json에 "dependencies": { "b": "workspace:*" } 추가 후
pnpm install7.3 코드 작성 및 실행
javascript
// packages/b/index.js
const axios = require('axios');
module.exports = async function fetchUsers() {
const response = await axios.get('https://api.github.com/users');
return response.data;
};javascript
// packages/a/index.js
const fetchUsers = require('b');
(async function main() {
const users = await fetchUsers();
console.log(users.map(u => u.login).join(', '));
})();bash
# 실행
pnpm --filter a start7.4 node_modules 구조 확인
bash
# 심볼릭 링크 확인
ls -al packages/b/node_modules/
# axios -> ../../node_modules/.pnpm/axios@1.6.0/node_modules/axios
ls -al packages/a/node_modules/
# b -> ../../packages/bpackages/a/node_modules/에는 b만 존재하고, packages/b/node_modules/에는 axios만 존재한다. 각 패키지가 자신의 직접 의존성만 볼 수 있는 엄격한 구조이다.
8. 마이크로프론트엔드에서 pnpm이 선호되는 이유
마이크로프론트엔드 아키텍처는 하나의 저장소에 여러 독립적인 앱과 공유 라이브러리를 관리하는 대규모 모노레포를 필요로 한다. pnpm은 이 환경에 가장 적합한 패키지 매니저이다.
8.1 선호 이유 요약
| 이유 | 상세 설명 |
|---|---|
| 디스크 효율 | 수십 개의 앱이 react, typescript 등을 공유해도 글로벌 스토어에 1회만 저장. 대규모 모노레포에서 GB 단위 절약 |
| 설치 속도 | 글로벌 스토어에 캐시된 패키지는 네트워크 요청 없이 하드링크만 생성. CI/CD 파이프라인 시간 단축 |
| 엄격한 의존성 | 팀별로 독립적으로 개발하는 마이크로프론트엔드에서 의존성 실수를 사전에 방지. Phantom Dependency로 인한 빌드 실패 원천 차단 |
| 강력한 필터링 | --filter로 변경된 패키지만 빌드/테스트 가능. 수십 개의 앱 중 영향 받는 것만 CI에서 처리 |
| 워크스페이스 프로토콜 | workspace:*로 내부 패키지 간 의존 관계를 명확하게 선언 |
| node_modules 호환 | Yarn Berry PnP와 달리 node_modules 기반이므로 기존 도구와의 호환성 문제가 거의 없음 |
| 생태계 성숙도 | Turborepo, Nx 등 모노레포 빌드 도구와 원활하게 통합 |
8.2 pnpm + 모노레포 빌드 도구 조합
pnpm은 자체적으로 워크스페이스를 관리하지만, 빌드 캐싱과 태스크 오케스트레이션은 전문 도구에 위임하는 것이 일반적이다.
bash
# pnpm + Turborepo 조합 예시
# turbo.json에서 파이프라인 정의, pnpm에서 의존성 관리
pnpm install
pnpm exec turbo run build --filter="[origin/main]"핵심 정리
- pnpm의 철학은 "빠르고 효율적"이다. 글로벌 콘텐츠 주소 기반 스토어에 패키지를 한 번만 저장하고 하드링크로 연결하여 디스크와 시간을 절약한다.
- 비-평탄 node_modules 구조에서 각 패키지의
node_modules에는 직접 의존성만 심볼릭 링크로 노출된다. 이로써 Phantom Dependency가 원천 차단된다. - 설치 3단계(해석 -> 페치 -> 링킹) 중 페치 단계에서 글로벌 스토어에 이미 있는 패키지는 건너뛰므로 설치 속도가 빠르다.
- pnpm-workspace.yaml로 워크스페이스를 선언하고,
--filter플래그로 특정 패키지를 정밀하게 타겟팅할 수 있다. - 마이크로프론트엔드의 대규모 모노레포에서 pnpm은 디스크 효율, 설치 속도, 엄격한 의존성, 강력한 필터링 덕분에 가장 적합한 패키지 매니저로 급부상하고 있다.
다음 단계
- 빌드 도구의 필요성: 패키지 매니저로 의존성을 관리한 후, 소스 코드를 브라우저가 이해하는 정적 파일로 변환하는 빌드 도구의 역할과 필요성을 알아본다.