본문으로 건너뛰기

"package-manager" 태그로 연결된 1개 게시물개의 게시물이 있습니다.

모든 태그 보기

똑똑하게 라이브러리 관리하기: npm, yarn, pnpm

· 약 7분
김현재
개발자

들어가며

JavaScript 프로젝트를 시작하면 가장 먼저 하는 일이 npm install입니다. 너무 자연스럽게 사용하고 있지만, 패키지 매니저가 정확히 어떤 일을 하는지, 그리고 npm 외에 yarn과 pnpm은 어떤 차이가 있는지 궁금해진 적이 있을 것입니다.

이번 포스팅에서는 패키지 매니저가 무엇인지부터 시작하여, npm, yarn, pnpm 각각의 특징과 장단점을 비교하고, 마지막으로 모노레포 환경에서 pnpm이 주목받는 이유까지 정리해보려고 합니다. 저도 모노레포 환경에서 pnpm을 선호하고 있습니다!

패키지 매니저란?

패키지 매니저는 프로젝트에서 사용하는 외부 라이브러리(의존성)를 설치하고 관리하는 도구입니다.

우리가 코드를 작성할 때 import React from 'react'처럼 간단하게 외부 라이브러리를 가져다 쓸 수 있는 이유는 패키지 매니저가 그 뒤에서 많은 일을 해주기 때문입니다. ECMAScript 표준에서는 원래 절대 경로나 상대 경로로만 모듈을 import할 수 있지만, 패키지 매니저가 'react'라는 이름만으로도 올바른 파일을 찾을 수 있도록 환경을 만들어줍니다.

package.json에 버전 범위를 명시하면 패키지 매니저가 정확한 버전을 결정합니다.

{
"dependencies": {
"react": "^18.2.0"
}
}

여기서 ^캐럿(Caret) 기호로, 버전 범위를 지정하는 키워드 중 하나입니다.

버전 범위 키워드

npm은 Semantic Versioning(SemVer) 규칙을 따릅니다. 버전은 MAJOR.MINOR.PATCH 형태로 구성되며, 각각의 의미는 다음과 같습니다.

  • MAJOR: 기존 API와 호환되지 않는 변경
  • MINOR: 하위 호환되는 새로운 기능 추가
  • PATCH: 하위 호환되는 버그 수정

이 SemVer를 기반으로, package.json에서는 다양한 키워드를 사용하여 설치할 버전의 범위를 지정할 수 있습니다.

키워드예시범위설명
^ (캐럿)^18.2.0>= 18.2.0, < 19.0.0MAJOR 버전 고정, MINOR와 PATCH 업데이트 허용
~ (틸드)~18.2.0>= 18.2.0, < 18.3.0MAJOR와 MINOR 고정, PATCH 업데이트만 허용
없음 (정확)18.2.018.2.0정확히 해당 버전만 설치
**모든 버전최신 버전 설치
>=, <=>=18.0.0>= 18.0.0비교 연산자로 범위 지정

가장 많이 사용되는 것은 ^(캐럿)입니다. npm install로 패키지를 설치하면 기본적으로 캐럿이 붙습니다. 캐럿은 MAJOR 버전이 바뀌지 않는 선에서 최신 버전을 허용하기 때문에, 하위 호환성을 유지하면서도 버그 수정이나 새로운 기능을 자동으로 반영할 수 있습니다.

이처럼 버전 범위 안에서 어떤 버전을 실제로 설치할지 결정하는 것이 패키지 매니저의 역할입니다.

패키지 매니저의 동작 과정

패키지 매니저는 크게 세 단계를 거쳐 동작합니다.

Resolution

모든 의존성의 정확한 버전을 결정하는 단계입니다. ^18.2.0과 같은 범위를 18.3.1처럼 구체적인 버전으로 고정하고, 의존성이 가진 의존성(transitive dependencies)까지 재귀적으로 탐색합니다.

이 결과는 package-lock.json이나 yarn.lock 같은 lock 파일에 저장되어, 다른 환경에서도 동일한 버전을 설치할 수 있도록 보장합니다.

Fetch

결정된 버전의 패키지 파일을 npm 레지스트리에서 다운로드하는 단계입니다.

다운로드한 패키지를 소스 코드에서 실제로 사용할 수 있도록 연결하는 단계입니다. 이 단계에서 npm, yarn, pnpm의 접근 방식이 크게 달라집니다.

npm

npm(Node Package Manager)은 Node.js와 함께 기본으로 제공되는 가장 오래된 패키지 매니저입니다.

동작 방식

npm은 node_modules 디렉토리에 패키지를 직접 설치합니다. 각 패키지가 자신만의 node_modules를 가질 수 있어, 중첩된 구조가 만들어집니다.

my-project/
└─ node_modules/
├─ react/
└─ some-library/
└─ node_modules/
└─ another-library/

의존성을 찾을 때는 현재 디렉토리의 node_modules부터 시작하여 상위 디렉토리로 올라가며 탐색합니다.

장점

  • 별도 설치 불필요: Node.js에 기본 포함되어 있어 추가 설치가 필요 없습니다.
  • 높은 호환성: 가장 널리 사용되므로 대부분의 패키지와 도구가 npm을 기본으로 지원합니다.
  • 낮은 학습 곡선: 별다른 설정 없이 바로 사용할 수 있습니다.

단점

  • 느린 설치 속도: 파일 I/O가 많아 설치 속도가 느립니다.
  • 디스크 용량: 같은 패키지가 여러 곳에 중복 설치될 수 있어 node_modules 용량이 매우 커질 수 있습니다.
  • 유령 의존성(Phantom Dependency): hoisting 최적화로 인해 package.json에 명시하지 않은 패키지도 사용할 수 있는 문제가 발생합니다.

레거시 버전의 npm은 hoisting이 일어나지 않습니다. npm 최적화를 위해 버전업과 함께 hoisting 기능이 추가되었습니다.

yarn

yarn은 Facebook(현 Meta)에서 개발한 패키지 매니저로, npm의 단점을 보완하기 위해 등장했습니다. 특히 yarn v2 이후 도입된 PnP(Plug'n'Play) 방식이 핵심적인 차별점입니다.

PnP(Plug'n'Play)

PnP는 node_modules 디렉토리 자체를 없애고, JavaScript 객체(Map)로 의존성 관계를 관리하는 방식입니다.

yarn install을 실행하면 .pnp.cjs 파일이 생성됩니다.

// .pnp.cjs (간략화)
[
'my-project',
[
{
packageLocation: './',
packageDependencies: [['react', 'npm:18.2.0']],
},
],
][
('react',
[
[
'npm:18.2.0',
{
packageLocation: './.yarn/cache/react-npm-18.2.0-...',
packageDependencies: [['loose-envify', 'npm:1.4.0']],
},
],
])
];

Node.js가 이 Map을 메모리에 올려두고, import나 require가 호출될 때 파일 시스템을 순회하는 대신 Map에서 바로 경로를 찾아줍니다.

장점

  • 빠른 설치 속도: 파일 하나(.pnp.cjs)만 생성하면 되므로 설치가 매우 빠릅니다.
  • 빠른 실행 속도: 파일 시스템 순회 없이 Map 연산으로 모듈을 찾아 실행 속도도 빠릅니다.
  • 엄격한 의존성 관리: package.json에 명시하지 않은 패키지는 사용할 수 없어 유령 의존성 문제가 발생하지 않습니다.
  • 플러그인 아키텍처: 코어 외 대부분의 기능이 플러그인으로 구성되어 있어 확장성이 뛰어납니다.

단점

  • 호환성 이슈: node_modules가 없기 때문에 node_modules의 존재를 가정하는 도구들과 호환되지 않을 수 있습니다.
  • 높은 학습 곡선: PnP 개념 자체가 기존 방식과 다르기 때문에 초기 학습이 필요합니다.
  • Node.js 프로세스 시작 속도: .pnp.cjs 파일을 메모리에 로드하는 과정에서 프로세스 시작이 다소 느려질 수 있습니다.

pnpm

pnpm(Performant npm)은 이름 그대로 npm의 성능을 개선하는 것에 집중한 패키지 매니저입니다.

동작 방식

pnpm은 node_modules 디렉토리를 유지하면서도 Hard LinkContent-Addressable Storage 방식을 사용합니다.

모든 패키지 파일은 전역 저장소(~/.pnpm-store)에 한 번만 저장되고, 각 프로젝트의 node_modules에는 이 파일들의 하드 링크가 생성됩니다. 파일을 실제로 복사하는 것이 아니기 때문에 디스크 용량을 크게 절약할 수 있습니다.

my-project/
└─ node_modules/
├─ .pnpm/ # 실제 패키지들 (하드 링크)
│ ├─ react@18.2.0/
│ └─ lodash@4.17.21/
├─ react -> .pnpm/react@18.2.0/ # 심볼릭 링크
└─ lodash -> .pnpm/lodash@4.17.21/ # 심볼릭 링크

장점

  • 빠른 설치 속도: npm 대비 매우 빠릅니다. 이미 전역 저장소에 있는 패키지는 다시 다운로드하지 않습니다.
  • 디스크 용량 절약: 하드 링크 방식 덕분에 같은 패키지를 여러 프로젝트에서 사용해도 실제 파일은 하나만 저장됩니다.
  • 엄격한 의존성 관리: package.json에 명시하지 않은 패키지에 접근할 수 없어 유령 의존성 문제를 방지합니다.
  • 높은 호환성: node_modules 구조를 유지하기 때문에 기존 도구들과의 호환성이 우수합니다.

단점

  • yarn PnP 대비 느림: 여전히 파일 시스템을 순회하는 방식이기 때문에 yarn PnP보다는 느립니다.
  • 하드 링크 관련 이슈: 일부 환경에서 하드 링크가 지원되지 않거나, 파일 수정 시 다른 프로젝트에도 영향을 줄 수 있는 잠재적 위험이 있습니다.

비교 요약

항목npmyarn (PnP)pnpm
설치 속도느림매우 빠름빠름
디스크 용량작음매우 작음
호환성우수낮음우수
유령 의존성 방지XOO
학습 곡선낮음높음낮음

모노레포에서 pnpm을 사용하는 이유

모노레포(Monorepo)는 하나의 저장소에서 여러 프로젝트(패키지)를 관리하는 방식입니다. 모노레포 환경에서 pnpm이 자주 선택되는 데에는 몇 가지 이유가 있습니다.

워크스페이스 지원이 강력하다

pnpm은 pnpm-workspace.yaml 파일 하나로 워크스페이스를 설정할 수 있습니다.

packages:
- 'packages/*'
- 'apps/*'

각 패키지 간의 의존성 관리, 스크립트 실행, 버전 관리 등을 체계적으로 지원합니다. --filter 옵션을 통해 특정 패키지만 대상으로 명령어를 실행하는 것도 간편합니다.

# 특정 패키지에서만 빌드 실행
pnpm --filter @my-org/web build

# 특정 패키지와 그 의존성까지 포함하여 빌드
pnpm --filter @my-org/web... build

디스크 용량 절약 효과가 극대화된다

모노레포에서는 여러 프로젝트가 같은 패키지를 공유하는 경우가 많습니다. npm은 각 프로젝트마다 동일한 패키지를 중복 설치하지만, pnpm은 하드 링크를 통해 하나의 파일만 유지합니다. 프로젝트 수가 많아질수록 이 차이는 더 커집니다.

엄격한 의존성 관리가 필수적이다

모노레포에서는 여러 패키지가 서로 다른 의존성을 가지고 있어, 유령 의존성 문제가 발생하면 디버깅이 매우 어려워집니다. pnpm의 엄격한 의존성 격리는 각 패키지가 자신이 명시한 의존성만 사용하도록 보장하여, 이런 문제를 사전에 방지합니다.

높은 호환성을 유지한다

yarn PnP도 성능과 엄격한 의존성 관리 측면에서 우수하지만, node_modules를 제거하는 접근 방식 때문에 일부 도구와의 호환성 문제가 발생할 수 있습니다. pnpm은 node_modules 구조를 유지하면서도 위의 장점들을 제공하기 때문에, 기존 도구나 라이브러리와의 호환성을 유지하면서 모노레포의 이점을 누릴 수 있습니다.

이러한 이유로 Turborepo, Nx 같은 모노레포 도구들도 pnpm을 기본 패키지 매니저로 권장하거나 강력하게 지원하고 있습니다.

마무리

지금까지 패키지 매니저의 개념부터 npm, yarn, pnpm의 특징과 차이점까지 살펴보았습니다.

정리하자면, npm은 안정적이고 호환성이 높지만 성능 면에서 아쉽고, yarn PnP는 혁신적인 접근 방식으로 성능과 정확성을 극대화했지만 호환성이라는 과제가 있으며, pnpm은 성능과 호환성 사이에서 좋은 균형을 이루고 있습니다. 프로젝트의 규모와 요구사항에 따라 적절한 패키지 매니저를 선택하는 것이 중요합니다.

참고