본문 바로가기
개발관련/NextJS(FE)

패키지 매니저의 대한 고찰 npm, yarn, pnpm

by yjoo_ 2024. 6. 17.

NextJS를 시작하기 앞서 몇가지 개념을 잡고 가야겠다는 생각이 들었다.

 

첫 번째로 패키지 매니저에 대해서 다뤄볼까 한다.

 

패키지 매니저?

어느정도 개발을 접해본 사람이라면 npm, pip, apt, brew같은 패키지 매니저를 다뤄본 적 있을 것이다.

 

이러한 패키지 매니저들은 웹 사이트에서 패키지를 다운받아 컴퓨터에 저장하는 역할을 한다.

 

또한 패키지들의 의존성, 버전 관리, 자동화 스크립트 실행 등등을 수행한다.

 

이번에 다룰 패키지 매니저는 3가지. npm, yarn, pnpm이다.

1. npm?

 

흔히들 Node Package Manager의 줄임말로 알고 있으나, 공식적으론 줄임말이 아닌 npm 그대로다.

 

npm 공식 문서에 따르면 기존의 "pkgmakeinst", 줄여서 "pm"을 진화시킨 작품으로

 

굳이 따지자면 Node pm인셈.

1.1 Package.json

npm의 핵심은 package.json에 있다.

{
  "name": "nextjs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "14.1.3",
    "react": "^18",
    "react-dom": "^18"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "typescript": "^5"
  }
}

 

구조를 살펴보면 프로젝트 버전과 테스트를 위한 자동 스크립트

 

그리고 사용하는 패키지와 의존성 패키지 버전까지 모두 저장되어있다.

 

잘 읽어보면 ^18처럼 패키지의 범위(range)를 표시하고 있다.

캐럿(^) 기호는 18버전을 고정하면서, 마이너 버전과 패치 버전은 업데이트를 허용한다.
즉, "^18.0.0"은 18.x.x 버전 중 가장 최신 버전을 사용하겠다는 의미다.
따라서 18.1.0, 18.2.3 등의 버전이 설치될 수 있지만, 19.x.x 버전은 설치되지 않는다.

 

하지만 이렇게 패키지 버전 관리를 하게되면 한가지 문제가 생기게 된다.

 

package.json을 통해 패키지를 설치했을 때 패치 버전이 달라지면서 에러가 생길 수도 있다는 것

 

이를 해결하기 위해 나온 것이 package-lock.json이다.

1.2 package-lock.json

package-lock.json은 npm 명령을 통해 node_modules가 변경되거나 할 때 수행된다.

 

해당 파일을 잘 살펴보면 버전이 명확하게 표기되는 것을 알 수 있다.

    "node_modules/@types/react": {
      "version": "18.2.66",
      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz",
	(생략)

 

패키지를 명확하게 설치하기 위함인데, 이쯤에서 드는 의문점 두가지.

 

1. package.json엔 왜 버전을 명확하게 표기하지 않을까??

 

2. 이러면 package.json은 필요 없는거 아니야?

 

1.2.1 package.json엔 왜 버전을 명확하게 표기하지 않을까??

만약 package.json에 패키지 버전명을 정확히 명시하게 된다면, 크고 작은 패키지들이 새로 업데이트 될 때 마다

 

새로운 버전을 추적하고 수정해주어야 한다.

 

하지만 캐럿(^)을 사용해 범위 표시해줄 경우, 수정없이 항상 최신 버전을 유지할 수 있게 된다.

1.2.2 package.json 필요없는거 아니야?

어디까지나 package-lock.json은 package.json을 보조하기 위한 파일이다.

 

패키지를 설치하기 위한 기준이 되며, 이를 바탕으로 일관성 있는 패키지 설치가 가능한 것이다.

1.3 npm의 장점과 단점

npm의 장점은 최대 규모의 패키지 레지스트리 덕에 활성화 된 커뮤니티와 높은 접근성을 갖고 있다.

 

nodeJS에 기본 내장되어 있어 따로 설치가 필요하지 않다는 것도 큰 장점이다.

 

허나 단점도 만만치 않은 편인데

 

1.3.1 보안 취약점 발생

faker와 colors라는 패키지의 개발자가 공짜로 대기업의 서포터가 되지 않겠다며 악성코드를 추가하여 새 버전을 배포하게 된 사건이 있었다.

 

이 때문에 faker를 의존성으로 사용하던 수많은 패키지들이 피해를 입은 사건이 발생했었다.

 

1.3.2 용량문제.

패키지를 설치하면 당연히 의존성 패키지가 존재한다.

 

서로 다른 두개의 패키지가 같은 의존성 패키지의 다른 버전을 사용한다면 어떻게 될까?

 

node_modules에는 같은 이름의 패키지가 설치되지 않는다. 먼저 설치된 의존성 패키지는 냅둔다.

 

그리고 새롭게 설치된 패키지 내부에 node_modules를 새롭게 만들어서 의존성 패키지를 설치한다.

node_modules 속의 node_modules

 

당연히 의존성 트리가 깊어질수록 설치 시간이 길어지고 용량을 많이 먹게 된다.

 

npm또한 이러한 중복을 최소화하기 위해 호이스팅을 도입하는 등 개선했지만 유령 의존성 문제가 발생하게 된다.

 

이에 대해선 yarn과의 공통적인 문제였으니 후술하도록 하겠다.

2. Yarn

yarn은 npm의 대안으로 등장하게 된 패키지 매니저이다.

 

페이스북과 구글의 개발자들이 만든 패키지 매니저로 npm의 단점을 보완하고자 만들었다.(지금은 큰 차이가 없다더라)

 

2.1 차이점

npm의 단점은 크게 속도(performance), 안정성(stability), 보안성(security) 등이 있다.

 

하나씩 비교해보자.

2.1.1 속도

yarn은 패키지 설치 시 캐시를 저장하여 중복 데이터를 설치하지 않는다.

 

그리고 npm이 패키지를 순차적으로 설치하는 것과 달리 병렬 설치를 지원해 performance가 향상되었다.

2.1.2 안정성(stability)과 보안성(security)

npm은 패키지 설치 시 자동으로 의존성과 코드를 실행시킨다.

 

무슨 말인가 하면 악성코드가 숨어있는 패키지가 의존되고 있다면 npm이 해당 의존성을 설치하면서 코드를 실행할 수 있다는 것.

 

자동으로 의존성이 설치되는 것이 위협이 없다면 편리한 특징이지만, 반대로 위험하기도 하다는 뜻이다.

 

그래서 yarn의 경우 yarn.lock을 통해 패키지를 설치할 때 checksum 검사를 수행하여 기존에 설치된 패키지와 온전히 동일한 파일인지 검사한다.

 

이를 통해 버전 차이로 인한 버그를 방지할 수 있고, 변조된 배포 버전을 피할 수 있다고 한다.

 

※ npm에서도 .npmrc를 통해 자동 실행 기능을 제한할 수도 있다고 한다.

2.2 yarn의 장점과 단점

yarn은 npm의 단점을 보완하고자 나온만큼 차이점의 장점이라고 봐도 무방하나, npm 또한 많이 발전하고 보완했기에 안정성과 속도면에서 많이 개선되어 큰 차이가 없다.

 

단점으로는 npm과 달리 yarn은 아래 명령어를 통해 설치해주어야 한다.

npm install yarn --global

2.3 유령 의존성(Ghost Dependency)

yarn과 npm의 공통적인 단점인 유령 의존성에 대해 간단하게 다뤄보겠다.

 

npm의 경우 중복 패키지가 설치되는 경우가 많다고 했다. 마찬가지로 yarn또한 같은 방식으로 중복 패키지가 많은데

 

중복을 최소화 하기 위해 위해 호이스팅 기법을 사용했다.

그림을 보면 중복 패키지를 모두 찾아 최상단에 통합시키는 방법을 사용한다.

 

그런데 이렇게 되면 내가 설치한 적 없는 패키지가 존재하게 되는데, 이 부분에서 문제가 발생한다.

 

내가 사용하는 패키지 A의 특정 코드에 사용한다는 이유만으로 의존성 패키지를 설치하게 되는데

 

빌드 속도가 저하되고 보안 취약점이 발생할 수도 있다. (악성코드가 있다면?)

 

이를 해결하기 위해 yarn berry가 나왔다.

2.3.1 Plug'n'Play를 사용하여 문제를 해결한 yarn berry

yarn berry는 node_modules를 버리고 .zip에 패키지 정보를 저장한다.

 

이 패키지의 정보들을 찾기 위해 pnp.cjs에 의존성 트리를 저장하여 제공한다.

 

당연히 용량도 작아져서 git에 관리하면서 zero-install로 패키지를 관리할 수 있게 된다.

 

획기적인 변화라고 할 수 있다. 하지만 아직 모든 패키지가 PnP를 지원하지 않는다.

3. pnpm

yarn과 npm의 대안으로 출시된 pnpm(performant npm) 말 그대로 효율적인 npm이라는 뜻이다.

 

개인적으로 굉장히 인상이 깊은 패키지 매니저였는데 아마 npm과 yarn을 쓰레기통에 버린 이미지가 탓인 것 같다.

npm 9.4v 부터는 pnpm과 비슷하게 isolated node_modules를 지원한다.

 

pnpm은 기존 yarn과 npm의 공통적인 문제인 유령 의존성을 해결하기 위해 나온 패키지 매니저다.

3.1  pnpm의 해결법

pnpm은 패키지를 설치할 때 다음과 같은 설치 과정을 거친다.

  1. 사용자가 pnpm install을 통해 패키지를 요청한다.
  2. pnpm은 Content-addressable Store(이하 CAS)에서 요청된 패키지의 정보를 조회한다.
  3. 해당 패키지가 전역 저장소에 존재하지 않으면 원격 저장소에서 패키지를 받아 CAS에 설치한다.
  4. 프로젝트 내부의 .pnpm에 패키지의 하드 링크를 저장한다.
  5. node_modules에 .pnpm의 패키지들의 심볼릭 링크를 저장한다.

이런 식으로 용량과 중복 패키지, 유령 의존성을 모두 해결하게 된다.

 

Content-addressable store (출처: https://pnpm.io/ko/motivation)

 

저장소의 패키지를 심볼릭 링크로 참조만 하는 것이기 때문에, 명시된 패키지만 사용할 수 있다.

 

그렇다고 node_modules가 아예 용량이 없는 것은 아니다.

 

자세한건 하드링크심볼릭 링크에 대해서 자세히 알아보면 된다.

4. 결론

세가지 패키지 매니저를 비교해본 결과 든 생각은 pnpm을 사용해보자였다.

 

yarn berry를 쓰기에는 PnP를 지원하지 않는 패키지가 아직은 많다는 것이었고, 그렇다고 유령 의존성 문제를 달고가기엔 찝찝했다.

 

사실 별 차이도 없지만 npm은 이미 자주 다뤄봤으니 pnpm도 다뤄보자는 것이 결론이었다.

 

그래도 이렇게 자세한 내용을 공부하면 꽤 재밌지 않은가?

 

프로그램 개발 역사를 공부함으로써 개발할 때 다양한 발상에 도움을 줄 것이다.