Skip to content

pnpm 워크스페이스

pnpm의 하드링크/심볼릭 링크 기반 의존성 관리 전략과 비-평탄 node_modules 구조를 이해하고, 마이크로프론트엔드 환경에서 pnpm이 선호되는 이유를 파악한다.

학습 목표

  1. pnpm이 추구하는 철학(빠르고 효율적인 패키지 관리)을 이해한다.
  2. 비-평탄(non-flat) node_modules 구조의 동작 원리를 파악한다.
  3. 하드링크와 심볼릭 링크 전략이 디스크를 절약하는 메커니즘을 이해한다.
  4. 엄격한 의존성 관리로 Phantom Dependency를 방지하는 방식을 이해한다.
  5. pnpm-workspace.yaml 설정과 워크스페이스 명령어를 익힌다.
  6. 마이크로프론트엔드에서 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 -v

corepack use 명령을 사용하면 package.jsonpackageManager 필드가 추가되어, 프로젝트에서 다른 패키지 매니저를 사용하려 할 때 경고를 표시한다.


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/.../axios

npm/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-store

3.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만 존재한다. axiosb가 사용하는 의존성이지만, anode_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.js

6. 워크스페이스 명령어

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 install

6.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 build

6.3 스크립트 실행

bash
# 특정 워크스페이스의 스크립트 실행
pnpm --filter a start

# 모든 워크스페이스에서 실행
pnpm -r run build

# 워크스페이스 루트까지 포함
pnpm -r --include-workspace-root run start

# 의존성 목록 확인
pnpm -r list

6.4 유용한 설정 (.npmrc)

ini
# .npmrc
# 호이스팅 패턴 설정 (특정 패키지만 호이스팅 허용)
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*

# 엄격 모드 (기본값: true)
strict-peer-dependencies=true

# 심볼릭 링크 대신 하드링크 사용
symlink=true

7. 실습: 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 install

7.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 start

7.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/b

packages/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]"

핵심 정리

  1. pnpm의 철학은 "빠르고 효율적"이다. 글로벌 콘텐츠 주소 기반 스토어에 패키지를 한 번만 저장하고 하드링크로 연결하여 디스크와 시간을 절약한다.
  2. 비-평탄 node_modules 구조에서 각 패키지의 node_modules에는 직접 의존성만 심볼릭 링크로 노출된다. 이로써 Phantom Dependency가 원천 차단된다.
  3. 설치 3단계(해석 -> 페치 -> 링킹) 중 페치 단계에서 글로벌 스토어에 이미 있는 패키지는 건너뛰므로 설치 속도가 빠르다.
  4. pnpm-workspace.yaml로 워크스페이스를 선언하고, --filter 플래그로 특정 패키지를 정밀하게 타겟팅할 수 있다.
  5. 마이크로프론트엔드의 대규모 모노레포에서 pnpm은 디스크 효율, 설치 속도, 엄격한 의존성, 강력한 필터링 덕분에 가장 적합한 패키지 매니저로 급부상하고 있다.

다음 단계

  • 빌드 도구의 필요성: 패키지 매니저로 의존성을 관리한 후, 소스 코드를 브라우저가 이해하는 정적 파일로 변환하는 빌드 도구의 역할과 필요성을 알아본다.