우테코 레벨3 팀 프로젝트를 진행하던 중, 평소에는 아무 생각 없이 사용하던 패키지 매니저인 npm에 대해 다시금 생각해보게 되는 계기가 생겼다. 어떤 팀은 yarn, 또 다른 팀은 pnpm, 심지어는 처음 들어보는 yarn berry를 사용하고 있었다. 이러한 이야기를 들으며 그동안 아무 고민 없이 npm만 사용해온 나에게는 꽤 큰 울림이 있었고, 이번 기회에 다양한 패키지 매니저에는 무엇이 있는지, 각각 어떤 특징을 갖고 있는지 직접 정리해보고자 한다.
패키지 매니저는 외부 라이브러리의 설치, 업데이트, 삭제, 버전 관리를 도와주는 도구다. 자바스크립트 프로젝트에서 패키지 매니저는 필수적인 역할을 하며, 다음과 같은 주요 기능을 제공한다.
패키지 설치
패키지 매니저를 사용하면 프로젝트에 필요한 라이브러리를 손쉽게 설치할 수 있다. 예를 들어, npm install react 또는 yarn add react 명령어로 React를 설치할 수 있다.
의존성 관리
하나의 패키지가 다른 여러 패키지에 의존하는 경우, 패키지 매니저는 이러한 의존성을 자동으로 해결해준다. 이를 통해 복잡한 의존 관계를 수동으로 관리할 필요 없이 필요한 모든 패키지를 함께 설치할 수 있다.
버전 관리
패키지 매니저는 패키지의 다양한 버전을 관리할 수 있다. 특정 버전을 명시적으로 설치하거나, 업데이트 시 버전 범위를 설정함으로써 프로젝트가 안정적인 버전의 패키지를 사용할 수 있도록 도와준다.
스크립트 실행
패키지 매니저는 build, test, deploy 등과 같은 프로젝트 스크립트를 실행하는 기능도 제공한다. npm run이나 yarn run 명령어를 통해 미리 정의된 작업들을 자동화할 수 있어, 개발과 배포가 훨씬 효율적이다
예를 들어, 우리가 자주 사용하는 React도 하나의 라이브러리이기 때문에, vite나 CRA(Create React App) 같은 툴을 사용하지 않는 경우 직접 npm install react 명령어를 통해 설치해야 한다. 이처럼 외부 라이브러리를 손쉽게 설치하고 관리할 수 있도록 도와주는 것이 바로 패키지 매니저다.
자바스크립트 생태계에는 다양한 패키지 매니저들이 존재한다. 예를 들어 npm, yarn, pnpm, yarn berry, entropic, rush 등이 있다.
이 중에서도 특히 많이 사용되고 유명한 패키지 매니저는 다음 네 가지다:
이번 글에서는 위 네 가지 패키지 매니저의 특징과 차이점을 중심으로 살펴보자!
Node.js 생태계의 가장 대표적인 패키지 매니저다. Node.js와 함께 기본적으로 설치되며, 많은 개발자들이 널리 사용하고 있다. 강력한 CLI(Command Line Interface)를 제공하여 패키지 설치, 버전 관리, 의존성 해결 등의 기능을 수행한다.
Node.js의 등장은 브라우저 외부에서도 자바스크립트를 실행할 수 있는 환경을 열어주었고, 이에 따라 코드의 모듈화를 통해 복잡한 애플리케이션을 구축할 필요가 생겼다.
자연스럽게 여러 모듈과 라이브러리를 일관된 방식으로 관리해야 하는 요구가 생겼고, 다수의 외부 라이브러리에 의존하는 구조 속에서 의존성 관리를 자동화할 수 있는 도구가 필요해졌다.
이러한 필요에 따라 등장한 것이 바로 npm 이다.
npm은 패키지의 설치, 업데이트, 삭제를 명령어 한 줄로 간단하게 수행할 수 있다.
또한 package.json 파일에 사용자 정의 스크립트를 설정할 수 있어, 빌드, 테스트, 배포 등 반복적인 작업을 자동화할 수 있다.
npm은 package.json 파일을 통해 프로젝트의 모든 의존성을 체계적으로 관리한다. 어떤 라이브러리가 필요한지 명시해두면, npm install 명령어로 해당 의존성과 하위 의존성까지 자동으로 설치할 수 있다. 덕분에 팀원 간 개발 환경을 일관되게 유지할 수 있게 되었다.
npm은 시맨틱 버저닝(Semantic Versioning) 규칙을 통해 패키지 버전을 관리한다. 각 버전은 major.minor.patch 형식으로 구분되며, 이를 기반으로 호환성 있는 업데이트와 버그 수정을 쉽게 적용할 수 있다. 이로 인해 패키지 간의 호환성을 안정적으로 유지할 수 있다.
npm은 전 세계에서 가장 큰 자바스크립트 패키지 저장소(npm registry)를 운영한다. 수백만 개의 패키지가 등록되어 있어, 필요한 거의 모든 라이브러리와 프레임워크를 쉽게 사용할 수 있다. 덕분에 개발자는 반복적으로 기능을 구현할 필요 없이, 검증된 패키지를 활용하여 개발 시간을 줄이고 제품 출시 속도를 높일 수 있다.
프로젝트에 너무 많은 패키지를 포함하게 되면, 의존성 관리가 점점 복잡해지고 충돌이나 호환성 문제가 발생할 수 있다.
예를 들어, 어떤 프로젝트에서 다음과 같은 상황이 있다고 가정해보자
이때 두 버전이 서로 호환되지 않는다면, 충돌이 발생하게 된다. 또한, packageA를 설치하면 그것이 의존하는 하위 패키지들까지 자동으로 설치되는데, 이처럼 의존성이 여러 단계로 얽히면 관리가 매우 어려워진다.
이러한 상황을 흔히 “의존성 지옥(Dependency Hell)”이라 부르며, 버전 충돌, 호환성 문제, 빌드 실패, 런타임 오류의 주요 원인이 된다.
npm의 초기 버전은 직렬 처리 방식으로 패키지를 설치했기 때문에, 많은 패키지를 설치할 때 시간이 오래 걸렸다. 특히 대규모 프로젝트에서는 의존성 트리가 복잡해지고, 이로 인해 설치 과정에서 많은 리소스를 소모하게 되어 성능 저하가 발생했다.
또한, 수많은 패키지가 설치되면서 node_modules 폴더의 용량이 급격히 커지는 문제도 있었다. 이는 디스크 공간을 낭비할 뿐 아니라, 빌드 속도와 로딩 시간에도 부정적인 영향을 미쳤다.
Yarn은 Facebook에서 개발한 패키지 매니저로, npm과 유사한 목적을 가지고 있지만, npm의 단점을 보완하고 성능과 안정성을 개선하기 위해 만들어졌다.
Yarn의 가장 큰 특징 중 하나는 병렬 설치 방식이다.
기존의 npm은 패키지를 직렬로 설치했기 때문에, 대규모 프로젝트에서는 설치 시간이 오래 걸리는 문제가 있었다. Yarn은 이를 개선하여 여러 패키지를 동시에 설치함으로써 설치 속도를 크게 향상시켰다.
또한 Yarn은 로컬 캐시 기능을 제공한다.한 번 다운로드한 패키지는 디스크에 저장되며, 이후 동일한 패키지를 설치할 때는 네트워크 요청 없이 캐시된 파일을 재사용하여 더 빠르고 효율적인 설치가 가능하다.
의존성 관리 측면에서도 yarn.lock 파일을 통해 모든 의존성의 정확한 버전 정보를 고정할 수 있어, 팀원 간의 환경 차이를 줄이고 더 안정적인 빌드 환경을 제공한다.
중첩된 의존성 구조로 인해 중복된 패키지가 여러 번 설치되는 문제가 발생했다. 예를 들어, 서로 다른 패키지가 같은 패키지의 서로 다른 버전에 의존한다면, 해당 패키지는 여러 번 중복 설치되며 디스크 공간 낭비와 성능 저하를 유발할 수 있다.
이러한 중복 문제를 해결하기 위해 호이스팅(Hoisting)이라는 메커니즘이 도입되었다. 호이스팅이란, 의존성 트리를 평탄화(flat)하여 가능한 한 많은 패키지를 프로젝트 최상위의 node_modules에 설치하는 방식이다. 이를 통해 같은 버전의 패키지(A@1.0, B@1.0 등)를 한 번만 설치하고 공유함으로써, 디스크 공간을 절약할 수 있다.
그러나 이 구조는 새로운 문제를 야기했다. 프로젝트에서 직접 명시하지 않았지만, 다른 패키지의 의존성으로 설치된 패키지에 암묵적으로 접근할 수 있게 되는 현상, 즉 유령 의존성(Phantom Dependency)이 발생한 것이다.
예를 들어, A 패키지가 B에 의존하고 있고, B는 package.json에 명시되어 있지 않더라도, node_modules에 함께 설치되기 때문에 프로젝트에서 B를 직접 사용할 수 있다. 문제는 A를 삭제할 경우 B도 함께 제거되므로, 이를 참조하고 있던 코드에서 런타임 오류가 발생할 수 있다.
결국 유령 의존성은:
pnpm (Performant npm)은 npm의 고성능 패키지 매니저로 npm과 Yarn(v1)의 효율성과 성능, 그리고 디스크 공간 사용에 대한 문제를 보완하기 위해 등장
pnpm은 모든 패키지를 중앙 저장소에 저장한다. 이 저장소는 일반적으로 사용자 홈 디렉토리 아래의 .pnpm-store에 위치하며, 각 패키지의 버전별로 실제 파일이 저장된다. 모든 pnpm 프로젝트는 이 중앙 저장소를 공유하므로, 동일한 패키지를 여러 번 설치할 필요 없이 재사용할 수 있다.
하드 링크는 중앙 저장소에 저장된 실제 패키지 파일을 가리키는 포인터다.프로젝트 디렉토리에서는 이 하드 링크를 통해 패키지를 참조하게 되며, 이 방식 덕분에 여러 프로젝트 간 중복 설치를 방지하고, 디스크 공간을 절약할 수 있다. 즉, 실제 파일은 한 번만 저장되고, 각 프로젝트에서는 그 파일을 직접 참조만 하기 때문에 성능과 저장 효율이 향상된다.
심볼릭 링크는 중앙 저장소에 있는 실제 패키지의 경로를 참조하는 포인터다. pnpm은 각 프로젝트의 node_modules에 심볼릭 링크를 생성하여, 의존성 패키지들이 중앙 저장소의 경로를 통해 간접적으로 연결되도록 한다.
이 구조의 장점은 다음과 같다:
pnpm은 npm 레지스트리와 완벽하게 호환된다. 따라서 기존에 npm을 사용하던 프로젝트에서도 pnpm으로 쉽게 마이그레이션할 수 있으며, npm에서 제공하는 대부분의 패키지를 문제 없이 사용할 수 있다.
또한 pnpm은 다양한 CLI 옵션을 제공하여, 프로젝트의 요구사항에 맞게 설치 방식과 구조를 유연하게 조정할 수 있는 장점도 있다.
pnpm은 npm 레지스트리를 기반으로 작동하므로 대부분의 패키지와 호환되지만, 모든 환경이나 프로젝트 설정에서 완벽하게 작동하는 것은 아니다. 특히 일부 복잡한 프로젝트에서는 pnpm의 독특한 node_modules 구조(심볼릭 링크 기반)가 문제를 일으킬 수 있다. 이는 특정 도구나 라이브러리가 전통적인 디렉토리 구조를 가정하고 동작할 경우, pnpm의 방식과 충돌할 수 있기 때문이다.
pnpm은 빠르게 성장하고 있지만, 아직까지는 npm이나 yarn에 비해 사용자 수와 커뮤니티 규모가 상대적으로 작다. 그로 인해 문제가 발생했을 때 자료나 해결책을 찾기 어려운 경우가 있다.
또한 일부 라이브러리는 pnpm을 공식적으로 지원하지 않거나, pnpm에서만 발생하는 호환성 문제나 버그가 발생할 수 있다.
기존의 Yarn Classic(v1)은 npm보다 더 빠른 설치 속도와 의존성 관리를 제공했지만 앞서 살펴본 바와 같이 유령 의존성 문제가 있었다.
이러한 패키지 매니저 자체의 구조적 한계를 보완하기 위해 Yarn Berry가 등장하였다.
PnP는 기존 방식처럼 node_modules 폴더를 생성하지 않고, .yarn/cache 폴더에 패키지를 압축된 형태(.zip)로 저장하고, .pnp.cjs 파일에 의존성의 위치 정보와 매핑을 기록하여 의존성을 직접 관리하는 방식이다.
my-project/
│
├── .yarn/
│ ├── cache/ # 패키지가 .zip 형태로 캐시되는 경로
│ └── unplugged/ # 압축이 해제되어 설치되는 패키지 경로
│
├── .pnp.cjs # 모든 의존성의 위치 정보를 담은 매핑 파일
├── .yarnrc.yml # Yarn 설정을 정의하는 YAML 파일
├── package.json # 프로젝트의 의존성과 스크립트 정의
└── yarn.lock # 의존성의 정확한 버전을 고정하는 파일
Yarn Berry는 .pnp.cjs 파일을 통해 프로젝트가 의존하는 패키지의 정확한 위치를 관리한다. 이를 통해 명시된 의존성 외의 패키지에는 접근할 수 없도록 제한하여, 유령 의존성이 발생할 여지를 원천적으로 차단한다.
즉
Yarn Berry는 PnP 방식을 통해 node_modules 폴더 없이도 프로젝트를 실행할 수 있도록 지원한다. 패키지와 의존성은 .yarn/cache 폴더에 .zip 파일 형태로 압축 저장되며, 이 캐시 폴더는 GitHub 저장소에 포함시킬 수 있다.
덕분에 새로운 개발자가 프로젝트를 클론하더라도, 별도의 yarn install 없이 곧바로 작업을 시작할 수 있다. 이는 패키지 설치에 소요되는 시간을 줄이고, 로컬 환경마다 다른 버전의 패키지가 설치되어 발생하는 의존성 충돌도 방지해준다.
또한, CI/CD 파이프라인에서도 패키지를 설치하는 단계가 생략되므로, 빌드 및 배포 속도가 빨라지고 시간과 리소스를 절약할 수 있다는 장점이 있다.
PnP는 node_modules 디렉토리를 사용하지 않고 패키지를 관리하는 방식이지만, 프로젝트에 사용하는 패키지 중 하나라도 PnP를 지원하지 않으면 node_modules 폴더가 생성될 수 있다. 이 경우 해당 패키지를 다른 대체 패키지로 변경하거나, 직접 PnP를 지원하도록 수정해야 할 수 있어, 호환성 문제가 발생할 수 있다.
Yarn Berry의 Zero-Install은 .yarn/cache 폴더에 의존성을 .zip 파일 형태로 저장하고 이를 Git 저장소에 포함시키는 구조다. 하지만 수백 개의 패키지가 포함된 대규모 프로젝트에서는 이 캐시 폴더의 크기가 상당히 커질 수 있다.
저장소 용량이 커지면 다음과 같은 문제가 발생할 수 있다
따라서 프로젝트 규모가 클수록 저장 전략에 대한 고려가 필요하다.
Yarn Berry는 기존 node_modules 방식과 구조적으로 다르기 때문에, 초기 설정에서 진입 장벽이 존재한다. 특히 PnP 방식을 처음 도입할 경우, 작동 원리나 설정 방식을 이해하는 데 시간이 걸릴 수 있다.
또한 Yarn Berry는 비교적 최근에 등장한 도구이기 때문에 일부 플러그인이나 도구는 아직 완전히 지원되지 않거나 개발 중이고, 문제 발생 시 커뮤니티나 자료가 부족해 빠르게 해결하기 어려울 수 있다.
평소에 아무 생각없이 사용하던 패키지 매니저를 다시한번 돌아보는 계기가 되었다. 이렇게 학습한 패키지 매니저 지식을 통해 다음 시간에는 기존 프로젝트에서 사용한 npm을 새로운 패키지 매니저로 마이그레이션을 해보는 시간을 가져보겠다.