프로젝트를 만들 때 항상 npm, Yarn Classic을 무의식중에 사용하고 있었습니다.
그러면서 패키지 매니저를 왜 사용해야 하고 어떤 패키지 매니저들이 있는지 궁금해하지 않고 사용하고 있었던 저를 알 수 있었고, 사이드 프로젝트에 사용할 패키지 매니저에 대해서 알아보고 어떤 패키지 매니저를 선택할지 저만의 기준을 세운 내용을 블로그에 기록하기로 했습니다.
초기의 자바스크립트의 개발 환경은 단순했기 때문에 HTML 파일 내에 직접 스크립트를 작성하거나, <script>
태그를 사용해 직접 로드하는 방식이었습니다.
이러한 방식들은 프로젝트에서 다수의 자바스크립트 라이브러리나 플러그인을 사용할 경우, 의존하는 라이브러리가 먼저 로드되기 위해서 개발자가 수동으로 스크립트의 순서를 관리해야 하는 번거로움이 있었습니다.
또한, 필요한 라이브러리를 사용하기 위해 직접 다운로드하여 프로젝트에 추가하거나, 추가한 라이브러리의 버전이 업데이트되면 다시 수동으로 새 버전을 다운로드하고 교체하는 번거로움과 많은 라이브러리들의 사용은 변수 충돌이나 스코프 문제가 발생했기 때문에 안정성과 유지 보수의 어려움을 겪었습니다.
Node.js가 등장하면서 서버 사이드에서 자바스크립트를 실행할 수 있게 되었고, 이런 환경에서 효과적으로 코드를 공유하고 재사용할 수 있는 시스템이 필요하게 되었습니다.
코드의 모듈화를 통해 복잡한 애플리케이션을 구축할 필요가 생겼고, 각각의 모듈과 라이브러리를 일관된 방법으로 관리해야 하는 필요성을 느끼게 되었으며, 다수의 외부 라이브러리에 의존하는 애플리케이션의 특성상 의존성 관리를 위해 자동화된 도구가 필요해졌습니다.
그렇게 패키지 관리 시스템이 필요하게 되면서 npm이 등장했습니다.
npm은 패키지의 설치, 업데이트, 삭제를 명령어로 간단하게 수행할 수 있으며, package.json
파일 내에 사용자 정의 스크립트를 설정할 수 있어 빌드, 테스트, 배포 등의 작업을 자동화할 수 있게 되어 개발 편의성을 높였습니다.
package.json
파일을 통해 프로젝트의 모든 의존성을 관리하며, 어떤 라이브러리들이 프로젝트에 필요한지 명시하여 npm이 자동으로 의존성을 해결할 수 있게 되었고, 모든 개발자가 package.json
에 명시된 대로 작업할 수 있게 되었습니다.
시맨틱 버저닝(Semantic Versioning) 규칙을 도입하여 패키지의 호환성을 유지하도록 지원했습니다. 시맨틱 버저닝 규칙은 의존하는 라이브러리의 버전을 major, minor, patch 로 나누어 관리하며, 호환성 있는 업데이트와 버그 수정을 쉽게 할 수 있도록 지원했습니다.
npm은 전 세계적으로 가장 큰 소프트웨어 레지스트리로, 수백만 개의 패키지가 저장되어 있어서 필요한 모든 자바스크립트 패키지에 접근할 수 있으며, 수많은 라이브러리와 프레임워크를 쉽게 사용할 수 있어 프로젝트의 개발 시간을 단축시키고 더 빠른 제품 출시가 가능해졌습니다.
프로젝트에 너무 많은 패키지를 포함하게 되면 의존성 관리가 복잡해지고 충돌이나 호환성 문제가 발생했습니다.
packageA
는 packageC v1.0
을 필요로 합니다.packageB
는 packageC v2.0
을 필요로 합니다.packageC
의 두 버전이 서로 호환되지 않을 경우 충돌이 발생하게 됩니다.packageA
를 설치했지만 packageA
가 다른 패키지에 의존하게 된다면 관련된 의존성 패키지들을 모두 설치하게 됩니다.이러한 의존성 지옥 문제는 버전 관리의 복잡성과 호환성 문제나 의존성 충돌로 인해 빌드 프로세스가 실패하거나 런타임 오류가 발생하게 되는 원인이었습니다.
npm의 패키지 설치 과정은 의존성 트리가 크고 복잡할 때 성능이 저하되었는데 그 이유는, 의존성 해결 과정에서 발생하는 계산이 시스템 리소스를 많이 사용하기 때문이었습니다.
의존성 패키지들이 많은 대규모 프로젝트라면 node_modules
의 크기가 상당히 커질 수 있다는 문제점이 발생하였습니다.
또한 초기 npm은 패키지를 하나 설치 받고 다음 패키지를 설치하는 직렬 처리 방식으로 설치 시간이 오래 소요되는 문제가 있었습니다.
npm은 시맨틱 버저닝 규칙을 사용한다고 했습니다.
하지만 이 규칙은 패키지 버전을 관리하는데 좋은 이점도 있지만 만약 minor, patch 릴리즈가 된 패키지임에도 예상치 못한 버그가 발생할 수 있었기 때문에 패키지 버전을 업데이트 하기 전에 충분한 테스트를 거쳐야 합니다.
시맨틱 버저닝 규칙은 개발자의 주관에 따라 달라질 수 있었으며, 패키지 개발자가 어떤 변경을 버그 수정(patch)으로 간주했지만 이 패키지를 사용하는 다른 사용자에게는 이러한 버전 변경이 기존의 기능과 충돌을 일으켜 major 변경으로 여겨질 수 있었고, 이런 문제는 예상치 못한 호환성 문제를 발생시켰습니다.
또한 package.json
에 캐럿(^
)으로 사용된 패키지들은 가장 최신의 minor 또는 patch 버전을 허용합니다.
예를 들어 ^1.2.3
은 1.2.3
이상 2.0.0
미만의 버전까지 설치할 수 있게 해주기 때문에 npm install
을 실행하면 새로운 버전이 자동으로 package.json
에 업데이트되기 때문에 업데이트된 버전에서 기존 코드와의 호환성이 안 맞는 경우 예상치 못한 버그를 발생할 수 있습니다.
만약 프로젝트에 추가 투입된 개발자가 프로젝트를 클론 받을 때, 클론 받는 시점의 패키지의 버전이 업데이트되었다면, 기존의 개발자와 패키지의 버전이 달라 호환성이 충돌할 수 있었고 내 컴퓨터에서는 되는데 다른 컴퓨터에서는 왜 안되지? 하는 등의 문제가 발생할 수 있었습니다.
이러한 자동 업데이트 문제는 package-lock.json
으로 일부 해결할 수 있는데, npm isntall
을 해도 package.json
의 버전에 따라 최신 버전을 자동으로 가져오는 대신 package-lock.json
에 명시된 버전을 기반으로 종속성을 설치하기 때문에 자동 업데이트로 인한 일부 문제를 해결할 수 있었지만 종속성 관리에 대한 전박적인 해결책이 되진 않았습니다.
Yarn Classic(Yarn v1)은 npm과 비슷하지만 더 나은 성능과 안정성, 보안을 제공하려는 목표로 등장하였습니다.
npm의 초기 버전은 패키지 설치 시 하나의 패키지가 완전히 설치된 후 다음 패키지가 설치되는 직렬 처리 방식이었기 때문에 대규모 프로젝트에서 설치 시간이 길어지는 문제가 발생했습니다.
Yarn(v1)은 여러 패키지를 동시에 설치하는 병렬 방식을 도입하여 설치 속도를 크게 향상시켰으며, 한번 다운로드한 패키지를 로컬 캐시에 저장하여 재 설치 시에 캐시 된 데이터를 사용함으로 네트워크 트래픽을 줄이고 설치 속도를 더욱 빠르게 향상시켰습니다.
npm의 초기 버전은 패키지 무결성(변조되거나 손상되지 않았음을 보증하는 방식)이나 체크섬(패키지 무결성을 보장하기 위해 사용되는 일종의 해시 값)이 제한되었습니다.
Yarn(v1)은 모든 패키지의 체크섬을 계산하고 이를 검증하는 과정을 통해 패키지 무결성을 강화하여 보안 문제를 방지하였습니다.
npm의 초기 버전은 package.json
파일 만으로는 프로젝트에 설치된 패키지 간의 정확한 의존성 관계와 버전이 명확하게 관리되지 않는 경우가 많았습니다.
Yarn(v1)은 yarn.lock
파일을 도입하여 프로젝트에 설치된 모든 패키지의 정확한 버전 정보를 자동으로 기록하고 관리하여 프로젝트가 어디에서 실행되어도 동일한 의존성을 보장하도록 했습니다. (이후 npm도 .lock
파일을 도입했습니다)
yarn.lock
파일의 도입으로 명시된 버전을 그대로 설치하여 다른 개발자나 배포 환경에서도 동일한 의존성 구조를 재현할 수 있게 되었습니다.
Yarn(v1)은 npm보다 더 나은 성능과 안정성을 제공했기에 대규모 프로젝트나 복잡한 의존성 관리가 필요한 프로젝트에서 많이 사용하게 되면서 빠르게 성장하였습니다.
하지만, Yarn(v1)도 npm과 같은 최상단 위치로 추출하고(호이스팅) 평탄화(flat)된 구조 방식의 한계점이 있었기 때문에 Yarn(v1) 개발진들은 추가 개발을 중단하고 유지 보수만 진행하기로 결정하였으며, 이후 Yarn Berry가 등장하게 되었습니다.
npm과 Yarn(v1)은 중복 의존성 문제와 유령 의존성 문제가 있었습니다.
npm과 Yarn(v1) 모두 초기 버전에서는 중첩된 의존성 구조로 인해 중복된 패키지가 여러 번 설치되는 문제가 발생하였습니다.
my-project/
├── node_modules/
│ ├── package-A/
│ │ ├── node_modules/
│ │ │ └── package-C@1.0.0/
│ │ └── package.json
│ ├── package-B/
│ │ ├── node_modules/
│ │ │ └── package-C@2.0.0/
│ │ └── package.json
│ └── package-C@1.5.0/
└── package.json
package-A
는 package-C@1.0.0
에 의존합니다.package-B
는 package-C@2.0.0
에 의존합니다.my-project
의 최상위 노드(node_modules
)에는 package-C@1.5.0
이 직접 설치되어있습니다.위의 폴더 구조는 package-C
가 세 번 중복되어 설치되며, 각각 다른 버전으로 관리하고 있기 때문에 디스크 공간을 비효율적으로 사용하고 버전 관리를 복잡하게 만들었습니다.
또한 npm은 패키지를 찾기 위해 계속 상위 폴더의 node_modules
를 탐색하는데 패키지를 바로 찾지 못한다면 최상위 폴더에 도달할 때까지 찾는 과정을 거쳐야 했기 때문에 이런 중복 의존성 문제는 패키지 탐색을 비효율적으로 만들었습니다.
중복 의존성 문제를 해결하기 위해 호이스팅 메커니즘이 도입되었습니다.
호이스팅 메커니즘이란, 패키지 매니저가 가능한 한 최상위 node_modules
에 패키지를 설치하여 평탄화(flat) 된 종속성 트리 모양으로 만드는 것을 말합니다.
이러한 평탄화(flat) 된 종속성 트리 모양의 구조는 A(1.0)과 B(1.0)을 두 번 설치하지 않고 한 번만 설치하기 때문에 디스크 공간을 줄일 수 있었습니다.
하지만 이로 인해 프로젝트에서 직접 의존하지 않는 패키지였던 B(1.0)을 암묵적으로 참조하게 되는 경우가 발생하였고, 이것을 유령 의존성 현상이라고 합니다.
유령 의존성 문제는 package.json
에 패키지가 명시되어 있지 않는다는 점이 가장 큰 문제였는데, 이러한 현상은 package.json
에 명시되지 않은 의존성에 접근할 수 있으며, A(1.0)을 삭제한다면 A(1.0)의 의존성 패키지인 B(1.0)도 삭제되었기 때문에 프로젝트의 의존성 관리를 어렵게 만드는 문제가 발생하였습니다.
pnpm (Performant npm)은 npm의 고성능 패키지 매니저로 npm과 Yarn(v1)의 효율성과 성능, 그리고 디스크 공간 사용에 대한 문제를 보완하기 위해 등장하였습니다.
pnpm은 node_modules
를 평탄화(flat) 된 종속성 트리 모양의 구조로 만들지 않고, 심볼릭 링크(Symbolic Links)를 사용하여 의존성을 관리합니다.
pnpm은 node_modules
디렉토리 구조에 심볼릭 링크를 사용하여 각 패키지가 오직 자신의 package.json
에 선언된 종속성만 접근할 수 있게 만들어 유령 의존성 문제를 해결했습니다.
Project/
├── node_modules/
│ ├── .pnpm/
│ │ ├── package-C@1.0/
│ │ └── package-D@1.0/
│ ├── package-A/
│ │ └── node_modules/
│ │ └── package-C@1.0 -> ../../.pnpm/package-C@1.0/
│ ├── package-B/
│ │ └── node_modules/
│ │ └── package-D@1.0 -> ../../.pnpm/package-D@1.0/
└── package.json
package-A
의 의존성 패키지는 package-C@1.0
입니다.package-B
의 의존성 패키지는 package-D@1.0
입니다.package-A
는 .pnpm
디렉토리 내에 있는 package-C@1.0
에 대한 심볼릭 링크를 갖습니다.package-A
가 package-D
를 사용하려고 시도한다면, 실패하게 되는데 그 이유는 package-A
의 node_modules
디렉토리에는 package-D
에 대한 링크나 복사본이 존재하지 않기 때문입니다.이러한 pnpm의 방식은 종속성 목록을 철저히 관리하게 되어 유령 의존성 문제를 해결합니다.
pnpm은 패키지 관리를 위해 중앙 저장소, 하드링크, 심볼릭 링크 방식을 도입하였습니다.
pnpm은 모든 패키지 파일을 중앙 저장소에 저장하며, 중앙 저장소에는 각 패키지의 버전 별로 실제 파일이 저장됩니다.
중앙 저장소는 일반적으로 홈 폴더 아래에 위치(.pnpm-store
)하며, 모든 pnpm 프로젝트에 공유되어 동일한 패키지를 다시 설치할 필요 없이 재 사용할 수 있습니다.
하드 링크는 중앙 저장소에 실제 저장된 패키지를 가리키는 포인터로, 여러 프로젝트에서 중앙 저장소에 있는 같은 패키지 파일을 참조하기 때문에 프로젝트별로 패키지 중복 설치를 방지할 수 있어 디스크 공간을 절약하며 패키지 속도를 개선할 수 있습니다.
심볼릭 링크는 프로젝트 내에서 중앙 저장소에 있는 실제 패키지 파일의 경로를 저장하는 참조 포인터로, 실제 저장된 패키지의 위치를 가리키고 있기 때문에 심볼릭 링크를 통해서 프로젝트에서 중앙 저장소에 저장된 패키지의 위치에 접근해서 사용할 수 있습니다.
심볼릭 링크는 각 패키지의 node_modules
에 필요한 의존성의 주소만 연결하며, 중앙 저장소의 패키지가 업데이트되면 심볼릭 링크를 통해 업데이트된 내용이 자동으로 반영되어 패키지의 일관된 버전을 관리할 수 있습니다.
즉, 하드링크는 중앙 저장소에 위치한 실제 패키지 자체를 가리키는 포인터이고, 심볼릭 링크는 중앙 저장소에 있는 패키지의 주소를 가리키는 포인터라는 차이점이 있고, 하드링크는 중앙 저장소의 실제 파일을 가리키기 때문에 하드링크를 통해 파일을 열거나 수정하면 실제 파일이 변경되지만 심볼릭 링크는 파일의 경로를 저장하는 포인터이기 때문에 원본 파일이 이동되거나 삭제될 경우 유효하지 않은 링크가 된다는 차이점이 있습니다.
pnpm의 가장 큰 특징은 중복된 패키지를 저장하지 않는 방식입니다. 중앙 저장소를 사용하여 패키지 파일을 저장하고 하드 링크와 심볼릭 링크를 사용하여 중앙 저장소에 저장된 하나의 동일한 패키지를 여러 프로젝트에서 공유할 수 있기 때문에 디스크 사용량을 줄여주며, 여러 프로젝트를 동시에 진행하는 개발 환경에서 매우 유용합니다.
pnpm은 중복된 패키지의 다운로드를 방지하며, 이미 설치된 패키지를 재 사용하며, 패키지 파일을 실제로 복사하는 대신 하드 링크를 사용하고 패키지들을 병렬 처리로 설치하는 방식이기 때문에 설치 속도를 더욱 향상시킵니다.
pnpm은 npm 레지스트리와 호환되기 때문에 pnpm을 사용하면서도 npm을 통해 관리되는 수많은 패키지들을 자유롭게 접근하고 사용할 수 있기 때문에 pnpm으로 마이그레이션하기 쉽다는 장점이 있습니다.
또한 다양한 옵션을 제공하여 프로젝트의 요구사항에 맞게 조정할 수 있습니다.
pnpm이 npm의 레지스트리와 호환되지만, 모든 환경과 프로젝트 설정에서 완벽하게 작동하지 않을 수 있습니다.
일부 복잡한 프로젝트에서는 pnpm의 node_modules
구조가 문제가 될 수 있는데, 패키지의 구조를 예상하는 특정 도구가 pnpm의 심볼릭 링크 방식을 제대로 처리하지 못할 수도 있다는 단점이 있습니다.
npm, Yarn(v1)에 비해서 상대적으로 사용자가 적기 때문에 문제가 발생했을 때 해결책을 찾기 어려울 수 있습니다. 또한 일부 라이브러리는 pnpm을 공식적으로 지원하지 않을 수 있고 이러한 경우 호환성 문제나 버그를 해결하기 위해 커뮤니티에서 도움받기가 어려워집니다.
기존의 Yarn Classic(v1)은 npm보다 더 빠른 설치 속도와 의존성 관리를 제공했지만 앞서 살펴본 바와 같이 유령 의존성 문제가 있었습니다.
이러한 패키지 매니저 자체의 구조적 한계를 보완하기 위해 Yarn Berry가 등장하였습니다.
Yarn Berry는 기존의 npm/Yarn(v1)의 유령 의존성 문제 해결 방법으로 Plug'n'Play(PnP)기능을 도입하였습니다.
PnP는 node_modules
폴더를 사용하지 않고, .yarn/cache
폴더에 의존성의 정보가 저장되어 .pnp.cjs
파일에 의존성을 찾을 수 있는 정보가 기록되는 방식입니다.
my-project/
│
├── .yarn/
│ ├── cache/ # 패키지 파일이 압축된 형태로 캐시되어 저장되는 폴더
│ └── unplugged/ # zip으로 묶이지 않고 압축 해제 된 종속성들이 설치되는 경로
│
├── .pnp.cjs # 의존성 해결 및 매핑을 관리하는 파일 (PnP 환경에서 사용)
│
├── .yarnrc.yml # Yarn 설정을 정의하는 YAML 파일
│
├── package.json # 프로젝트 의존성 및 스크립트 설정 파일
└── yarn.lock # 의존성의 정확한 버전을 고정하는 락 파일
node_modules
폴더를 생성하지 않고, .yarn/cache
폴더에 패키지 파일들을 .zip
파일로 압축하여 저장합니다..pnp.cjs
파일에서 기록하고 있기 때문에 필요한 패키지의 정확한 위치를 알려주고, 자신의 의존성 패키지 외에는 접근할 수 없어 유령 의존성 문제를 해결합니다.Plug'n'Play 방식은node_modules
를 생성하지 않고, .yarn/cache
폴더에 의존성과 관련된 정보들이 .zip
파일로 압축되어 저장하며, 각 패키지는 .pnp.cjs
파일에 패키지의 정확한 위치를 기록하기 때문에 자신의 의존성 패키지 외에는 접근할 수 없어 유령 의존성 문제를 해결하고 더 안정성 있게 사용할 수 있습니다.
이러한 PnP방식은 별도의 I/O 작업 없이도 패키지의 위치를 정확히 알 수 있기 때문에 속도가 매우 빠르며, 한 번 로드된 패키지는 메모리 내에서 캐싱 될 수 있어 이후 동일한 패키지 요청에 즉시 재사용되기 때문에 전체적인 성능을 향상시키는 특징이 있습니다.
PnP 방식으로 무거웠던 node_modules를 생성하지 않고 패키지와 의존성들이 .yarn/cache
폴더에 .zip
파일로 압축된 형태로 저장되어 있기 때문에 Github에 포함시킬 수 있습니다.
새로운 개발자가 프로젝트에 참여하여 프로젝트를 클론 하면 추가적인 패키지 설치 작업 없이 바로 작업을 시작할 수 있기 때문에 기존에 로컬 환경에서 모든 패키지를 설치해야 하는 시간과, 일부 패키지가 다른 버전으로 설치되어 의존성 충돌이 생기는 문제를 방지합니다.
또한, CI/CD 파이프라인에서 Zero-Install을 사용하면 패키지 설치 시간이 필요 없어져 배포 과정이 더 빠르기 때문에 시간과 리소스를 절약할 수 있습니다.
.yarn/cache/
: 패키지 파일들이 압축된 형태로 저장되며 의존성 패키지들을 로컬이 미리 다운로드하고 저장하기 때문에 한번 다운로드된 패키지는 여러 프로젝트에서 재 사용할 수 있으며, Zero-Install 기능과 함께 사용되어, 패키지 설치 과정을 생략할 수 있게 합니다..pnp.cjs
: Plug'n'Play (PnP) 기능의 핵심 파일로, 모든 의존성과 그 의존성들이 저장된 위치를 매핑하는 자바스크립트 파일로, 모듈 요청에 맞는 패키지의 정확한 위치로 리다이렉트합니다. node_modules
을 사용하지 않기 때문에 성능이 향상됩니다..yarnrc.yml
: Yarn 구성 설정을 정의하는 파일로, 네트워크 타임아웃, 캐시 위치, PnP 설정 등을 사용자가 조정할 수 있습니다.Plug'n'Play 기술을 사용하기 때문에 node_modules
폴더를 생성하지 않고 .pnp.js
파일을 통해 패키지를 직접 연결하는 방식이기 때문에 중복 파일을 방지하고 필요한 패키지만을 사용하고, .yarn/cache
에 압축된 형태(Zip)로 패키지를 저장하기 때문에 저장 공간을 효율적으로 사용할 수 있다는 장점이 있습니다.
Yarn Berry는 패키지를 동시에 다운로드하는 병렬 처리 방식을 사용함으로 설치 시간을 최소화하며, 한번 다운로드한 패키지를 로컬 캐시에 저장하여 필요할 때마다 재 사용하는 방식으로 빠른 성능을 제공합니다.
또한 패키지 간의 충돌을 방지하며 정의된 패키지만을 사용함으로 패키지 간의 호환성 문제를 최소화하고 안정성을 높이는 장점이 있습니다.
Yarn Berry는 사용자 정의 플러그인을 통해 기능을 추가하거나 변경할 수 있으며, 더 향상된 CLI를 제공합니다. 명령어와 옵션이 사용자 친화적으로 개선되었으며, 문제 발생 시 더 상세한 오류 메시지를 제공합니다.
Plug'n'Play 기능은 node_modules
디렉토리를 사용하지 않고 패키지를 관리하기 때문에 프로젝트의 패키지 중 하나라도 PnP 방식을 도입하지 않는다면 node_modules
폴더가 생성됩니다.
이러한 경우 다른 패키지로 변경하거나 PnP를 지원하도록 직접 개발해야 할 수 있습니다.
Yarn Berry는 .yarn/cache
폴더에 종속성의 .zip
파일이 저장되고 이 폴더가 Git 저장소에 포함되는 방식입니다.
물론 압축된 형태로 저장되지만, 수백 개의 패키지가 포함되어 있는 대규모 프로젝트에서는 많은 양의 데이터가 될 수 있으며, 저장소 크기가 커짐에 따라 Git 작업의 복잡성과 관리가 어려워질 수 있습니다. 커밋, 풀, 푸시 작업이 느려질 수 있고 저장소의 크기가 너무 커지면 일부 GIt 호스팅 서비스의 저장 용량 제한을 초과할 수 있다는 단점이 있습니다.
Yarn Berry는 PnP 환경을 구축하려고 할 때 기존의 node_modules
방식과 다른 차이점 때문에 초기 설치시 어려움을 겪을 수 있고, PnP 방식을 이해하기 위해 초기에 상당한 시간을 할애할 수 있습니다.
하지만 최근에 나온 도구이므로 아직 일부 플러그인들이 개발되지 않았을 수 있고, 문제 발생 시 적은 커뮤니티로 인해 도움을 받기 어려울 수 있습니다.
npm 레지스트리는 세계적으로 큰 레지스트리로 수백만 개의 패키지가 저장되어 있어 손쉽게 패키지를 설치하여 사용할 수 있습니다. 만약 프로젝트에 필요한 패키지가 의존하는 패키지에 악성코드가 심어져 있다면 프로젝트는 보안 문제를 일으킬 수 있습니다.
실제로 패키지 매니저들의 보안 취약점이 실제로 악용되는 사례는 여러 번 있었는데, 2018년에 eslint-scope
패키지가 npm 레지스트리에서 해킹되어 악성코드가 포함된 버전이 배포된 적도 있었습니다.
이러한 보안 문제를 해결하기 위해 npm6부터 audit
기능이 생겼고, 의존성을 검사하여 취약점에 대해 보고해 주지만 실제로 npm audit
명령어를 실행했을 때 보고대로 수정한다고 해도 한계가 있습니다.(관련 링크)
결국 패키지 매니저의 보안을 위해 npm audit
, yarn audit
, pnpm audit
와 같은 기능을 사용하며 주기적으로 확인하고(실제로 모든 게 해결되진 않지만), .lock 파일을 사용하여 의존성을 특정 버전에 고정시키거나 공개 레지스트리 대신 프라이빗 레지스트리를 사용한다는 등의 방법으로 보안에 대해 신경 써야 한다는 것을 기억해야 합니다.
npm 패키지 매니저를 사용하여 React 앱을 Docker 환경에서 AWS에 빌드했을 때
총 4분 2초가 걸리는 것을 확인할 수 있었습니다.
pnpm 패키지 매니저를 사용하여 React 앱을 Docker 환경에서 AWS에 빌드했을 때
총 3분 2초가 걸리는 것을 확인할 수 있었습니다.
같은 내용의 프로젝트를 설치 & 배포했을 때 확실히 npm 보다 pnpm이 빠른 것을 알 수 있었습니다.
YarnBerry 패키지 매니저를 사용하여 React 앱을 Docker 환경에서 AWS에 빌드했을 때
총 3분 0초가 걸리는 것을 확인할 수 있었습니다.
Yarn Berry가 가장 빨랐지만 YarnBerry의 PnP 방식을 Dockerfile로 이미지 빌드 했을 때 오류가 발생하여 관련 의존성을 결국 모두 COPY 하는 걸로 해결했기 때문에 사실 제대로 적용했다고 하긴 어려웠지만 그럼에도 불구하고 가장 빠른 것을 알 수 있었습니다.
저는 최종적으로 pnpm을 사용하기로 결정했습니다.
사실 제가 하려는 사이드 프로젝트는 크기가 작기 때문에 npm, yarn 을 사용해도 무리 없는 프로젝트이지만, 더 나은 성능을 제공하는 pnpm과 Yarn Berry를 도입해 보고 싶다는 생각이 들었습니다.
그중에서도 pnpm을 선택한 이유는 Yarn Berry는 node_modules 폴더를 생성하지 않는 방식이 신기하다고 생각했지만 반대로 안정성이나 호환성 문제에서 걱정이 되었습니다.
물론 제 작은 프로젝트에서는 큰 문제가 없겠지만 만약 회사에서 큰 규모의 프로젝트를 진행한다고 했을 때 Yarn Berry의 PnP 방식을 지원하지 않는 패키지들이 사용 중이라면 결국 node_modules
폴더가 생성될 테고 node_modules
를 사용하지 않기 위해 플러그인 개발을 추가로 들어가거나 패키지 호환성 문제를 해결해야 하는데 시간을 더 소요할 수 있다고 생각이 들었기 때문입니다.
그리고 이번에 속도 비교를 하면서 Dockerfile로 PnP를 COPY 하는 과정에서 오류가 발생해서 오류를 해결하기 위해 관련 자료들을 찾아봤을 때 확실히 자료가 적다는 느낌과, 비교적 수월하게 pnpm은 성공했던 것에 비해서 PnP 방식은 처음 도입하는 저의 입장에선 더 난이도가 높다고 느껴졌습니다.
Yarn Berry가 pnpm보다 엄청나게 더 좋은 이점이 특별히 없다면 저는 조금 더 호환성 면에서 안정성이 높은 pnpm을 사용하는 게 더 좋다고 판단되었습니다.
이번에 패키지 매니저를 공부하면서 정말 많은 것을 모르고 있었다는 생각이 들었지만 이전에 공부했던 도커를 활용해 보고 싶은 마음에 열심히 삽질하는 시간을 가져서 힘들었어도 재미있게 공부했던 것 같습니다.
앞으로도 프로젝트를 할 때, 왜 이런 기술들을 사용해야 하는지 호기심을 가져야겠다는 생각을 했습니다.
패키지 매니저가 발전한 흐름에 대해 상세하게 기술해주신게 아주 흥미로웠어요!
피드백드리고 싶은 부분이 있는데,
yarn에서 berry라는 것은 특정 버전을 지칭하는게 아니라 최신 버전을 berry라고 불러요.
PnP의 경우 yarn 2에서 공개되었고, 현재의 yarn berry는 version 4입니다! yarn의 각 버전별 특징에 대해서도 글에 담아주시면 글의 완성도가 더 올라갈 것 같아요!
그리고, yarn에서도 nodeLinker 설정을 통해 PnP 말고 node_modules를 사용할 수 있는데, 도커 트러블슈팅 하실 때 시도해보셨나요?!