Development Environment

Package Manager 패키지 매니저, pnpm

남희정 2023. 8. 9. 13:26

Package Manager 패키지 매니저

(패키지 관리 시스템)

컴퓨터의 운영 체제를 위해 일정한 방식으로 컴퓨터 프로그램의 설치, 업그레이드, 구성, 제거 과정을 자동화하는 소프트웨어 도구들의 모임이다. 패키지 관리자는 아카이브 파일로된 소프트웨어 배포판과 데이터인 패키지(package)를 다룬다.

  • 패키지 설치, 업데이트, 삭제
  • 의존성 dependency 관리
  • 패키지 압축 해제
  • 패키지 검색
  • 환경 설정
  • 보안 관리 : 신뢰할 수 있고(authenticity), 손상되지 않음(integrity)을 보장

Package?

Package는 소프트웨어나 프로그램 등의 실행 가능한 코드와 관련된 모든 파일들을 포함하는 하나의 단위를 말한다. 이런 파일들은 서로 연관되어 있어서 하나의 기능이나 작업을 수행하기 위해 필요한 모든 자원들이 함께 묶여 있는 형태로 제공된다. 쉽게 설명하면 배포를 위해 사용되는 코드의 묶음

 

  1. 컴파일한 소프트웨어의 바이너리(Binary)
    • 컴파일된 소프트웨어의 실행 가능한 바이너리 파일
    • 컴퓨터에서 직접 실행할 수 있는 실행 파일
  2. 환경 설정(Configuration)에 관련된 정보
    • 소프트웨어가 올바르게 동작하기 위해 필요한 구성 파일
  3. 의존 Dependency에 관련된 정보
    • 다른 패키지들과의 종속 관계

Package를 어디서 들고 올까?

패키지 매니저의 온라인 소프트웨어 저장소 software repository(repos) 혹은 레지스트리registry라고도 불림. software repository에서 패키지를 가져온다.

인터넷상에 호스팅되어 있고 다양한 패키지들과 그에 대한 메타데이터(패키지 정보)가 저장되어 있다.

대표적으로 JavaScript 프로젝트의 경우, npm과 yarn이 사용되는데, npm 레지스트리와 yarn 레지스트리라는 온라인 저장소에서 패키지를 가져온다.
즉, 레지스트리 안에 패키지가 있다. 레지스트리는 특정 프로그래밍 언어 또는 프로젝트 환경에 맞춰서 패키지들을 관리하고 배포하는 중앙 집중식 저장소이다.

  • 커뮤니티에 기여하는 것을 목적으로 다른 사용자들을 위해 패키지를 등록할 수도 있다.
  • 성능 문제(load balancing)와 위기상황 대처(fault tolerant)를 위해 여러 개로 분리되어있으며, 각각의 저장소가 동일한 기능을 수행한다.
  • Software repository에 대한 위치 정보를 관리하는 환경 설정 파일이 존재하므로, 그 경로를 참고하여 software repository에 접근하는 방식을 사용한다.

묶음은 어떤 기준으로 정해지지?

일반적으로 프로그래밍 언어 또는 개발 환경에 따라 정해짐. 패키지 매니저가 이를 관리한다.
패키지 매니저들은 특정 규칙과 규약을 따라 패키지를 생성하고 관리한다.
JavaScript 기준으로 root 디렉토리에 `package.json`파일을 사용한다. 패키지 매니저가 이 파일을 참조하여 프로젝트에 필요한 패키지를 설치하고 관리한다.

오 완전 흥미롭다. package.json이 패키지 단위를 제시하는거나 다름없구나!

`package.json`이 하는 일?

package.json파일은 프로젝트 정보와 의존성(dependencies)을 관리하는 문서이다.
아래는 npm 공식 문서에 나온 설명이다.


다른 사람들이 쉽게 관리하고 설치할 수 있도록 패키지에 `package.json`파일을 추가할 수 있고, 레지스트리에 게시된 패키지는 `package.json`파일을 포함해야 한다!

  • 프로젝트가 의존하는 패키지 리스트
  • semantic versioning rules를 사용하여 프로젝트에서 사용할 수 있는 패키지 버전을 지정한다.
    (프로젝트 버전을 명시)
  • 다른 환경에서도 빌드를 재생가능하게 만들어 다른 개발자가 쉽게 사용할 수 있도록 한다.

 

오픈소스 패키지 생태계를 사용하기 위한 명세이자, 프로젝트의 의존성 관리를 위한 명세, 또 이 생태계로의 배포를 위한 명세라고 볼 수 있다.

 

글을 참조한 개발자분이 스스로 정의하신 부분인데 너무 적절한 말 같아서 들고왔다.

package.json 구성요소는 정말 많아서 나중에 따로 정리해야겠다.

 

의존성 dependency?

의존성 Dependency 혹은 종속성은 정말 자주 듣고 본 단어지만 정확하게 제대로 짚고 넘어가자!

한 소프트웨어가 다른 소프트웨어에 의존하는 경우를 가리키는 광범위한 소프트웨어 엔지니어링 용어이다.
예를 들어, JavaScript 프로젝트에서 프로젝트 A가 프로젝트 B의 함수를 사용하려면 A는 B에 대한 의존성이 있다고 말한다.

애플리케이션이 복잡해질수록 의존성은 피할 수 없는 과제인 듯하다. 그렇기에 이걸 어떻게 잘 컨트롤할지 고민하는게 개발자의 몫인듯하다.

의존성과 관련된 글 중 흥미로운 부분을 들고왔다.

 

위험이 많을 수 밖에 없는데 왜 종속성을 사용할까?

애플리케이션은 날이 갈수록 점점 더 복잡해지고 있다. 애플리케이션 이면의 평균 코드 양도 날마다 증가하고 있기에 우리 각자가 매번 처음부터 시작한다면 거의 진전이 없을 것이다.

 

What are Dependencies in Programming

Why we use Dependencies?

 

우리는 이미 시도되고 테스트된 것을 수행한 다음 그 위에 추가로 구축합니다.
테스트된 코드를 사용하여 시간을 절약한 다음 절약한 시간을 사용하여 새롭고 더 나은 것을 만듭니다.
Python이 인기 있는 주요 이유 중 하나입니다.
다양한 시나리오를 위해 미리 작성된 코드가 포함된 Python 라이브러리가 너무 많아 새로운 프로그래머가 이러한 라이브러리를 사용하여 몇 주 내에 복잡한 고급 응용 프로그램을 빌드할 수 있습니다.

 

 

의존성은 일반적으로 두 가지의 경우에 나타난다.

 

컴파일 타임 의존성(Compile-time Dependency)
프로그램을 컴파일하는 단계에서 발생하는 의존성.
(C, C++, Java, Kotlin)

컴파일러는 해당 컴포넌트가 다른 모듈의 함수 또는 변수를 사용하고 있다면, 이를 찾아서 해당 모듈과의 의존성을 설정한다. 주로 정적 타입 언어에서 나타나며, 컴파일 타임에 의존성이 결정되므로 컴파일러가 해당 의존성을 검사하고 오류를 발생시킬 수 있다.
런타임 시에는 추가적인 의존성 로딩이 필요하지 않아서 애플리케이션의 시작 속도가 빠르고, 보다 안정적인 코드를 제공할 수 있다.

 

런타임 의존성(Runtime Dependency)
프로그램이 실행되는 동안 동적으로 발생하는 의존성.
(JavaScript, Python, Ruby, PHP)
런타임 의존성은 프로그램이 실행되는 도중에 프로그램 내부에서 또는 외부에서 다른 모듈이나 라이브러리를 호출하거나 사용할 때 발생한다. 주로 동적 타입 언어에서 나타나며, 런타임에 의존성이 결정되므로 실행 중에 의존성 오류가 발생할 수 있다.
애플리케이션이 실행 중에 필요한 모듈을 동적으로 로드하여 사용한다. 유연성을 제공하며 애플리케이션의 크기를 줄일 수 있다! 하지만 런타임에 의존성이 해결되기 때문에 의존성 오류가 발생할 수 있으며, 애플리케이션 실행 시에 의존성 로딩으로 인해 시작 속도가 상대적으로 느릴 수 있다.

 

yarn, yarn berry, pnpm

세가지 모두 Javascript 프로젝트에서 사용되는 패키지 매니저이다. 각각의 차이점을 알아보자!

yarn

  • Facebook에서 개발한 JavaScript 패키지 매니저
  • NPM과 유사한 기능을 제공
  • NPM보다 더 빠른 패키지 설치 속도와 의존성 관리를 제공
  • .yarnrc 파일을 통해 각종 설정을 관리
  • yarn.lock 파일을 통해 패키지의 버전 정보를 관리한다.
  • 대규모 프로젝트에서 유용하게 사용

yarn berry

  • yarn berry는 Yarn의 2 이상의 버전을 일컫는 말이다.
  • 이전 버전과 구조가 많이 다르다!
  • monorepo 구조나 플러그인 매커니즘을 강화했다.
  • 대규모 프로젝트에서 유용하며, 자체적으로 플러그인과 통합된 매커니즘을 갖고 있다.

pnpm

  • pnpm은 Yarn과 NPM과 다른 접근 방식을 가진 JavaScript 패키지 매니저
    • Yarn과 NPM은 패키지를 설치할 때 각각의 프로젝트마다 패키지를 복사하여 저장하는데, 이로 인해 디스크 공간을 많이 차지할 수 있다. 반면 pnpm은 하나의 공유 패키지 저장소를 사용하여 모든 프로젝트에서 패키지를 공유하고, 하드 링크를 통해 중복을 피한다.
  • 설치 속도가 빠르며, 패키지 설치를 효율적으로 처리한다.
  • 디스크 공간을 효율적으로 관리하고자 하는 경우에 유용하다.
  • 때문에 작은 프로젝트부터 대규모 프로젝트까지 사용 가능하다.

 

 

다른 패키지 매니저와 구조가 다르고, 모노레포에 최적화 되어있다고 하여 pnpm을 사용해보기로 했고, 어떻게 다른지 구체적으로 알아보려 한다!


pnpm

Zoltan Kochan에 의해 2017년에 출시되었다. npm에 대한 드롭 인 대체(Drop-in replacement) 이므로 npm 프로젝트가 있으면 바로 pnpm을 사용할 수 있다!

 

드롭 인 대체(Drop-in replacement)
컴퓨터 과학 및 기타 분야 에서 사용되는 용어. 다른 코드나 설정 변경 없이 하드웨어(또는 소프트웨어) 구성 요소를 다른 하드웨어의 (또는 소프트웨어) 구성 요소로 대체해도 부정적인 영향을 미치지 않는 기능을 나타낸다.

 

유령 의존성(phantom dependency)

yarn은 패키지들을 가져와서 설치할 때 병렬적으로 설치를 한다. 패키지가 여러개 있다면 순차적으로 설치하는 npm보다 yarn이 훨씬 빠르게 된다. 또한 오프라인 미러가 있어서 오프라인에서도 패키지를 받아 설치할 수 있다.

yarn은 npm의 좋은 대체재였지만 두 패키지 매니저 모두 몇 가지의 문제를 가지고 있었다. 그중 하나는 flat한 의존성 트리를 만드는 알고리즘의 비용이 많이 든다는 것. 그리고 또 다른 이유는 유령 의존성(phantom dependecy)이다.

 

예시
`my-library/package.json`

{
  "name": "my-library",
  "version": "1.0.0",
  "main": "lib/index.js",
  "dependencies": {
    "minimatch": "^3.0.4"
  },
  "devDependencies": {
    "rimraf": "^2.6.2"
  }
}

위 `package.json`으로 아래 `my-library/lib/index.js` 을 실행할 수 있다.

var minimatch = require("minimatch")
var expand = require("brace-expansion");  // ???
var glob = require("glob")  // ???

// (more code here that uses those libraries)

`package.json`에는 `brace-expansion` 패키지와 `minimatch` 패키지가 없지만 에러가 나지 않는다.
`brace-expansion`은 `minimatch`와 의존 관계에 있는 패키지이고 `glob`은 `rimraf`의 의존 관계에 있는 패키지다.

내가 사용하기 위해 입력한 패키지는 아니었지만 의존 관계 덕에 사용이 가능하다. 문제는 유령 의존성에 있는 패키지의 관리가 어렵다는 것이다.

  • 사용하지 않는 패키지 정리가 어렵다.
  • 버전 충돌 위험성 (어떤 버전을 사용할 것인지에 대한 내용이 없다.)

saving disk space

npm 및 Yarn Classic은 의존성 접근 방식이 동일 했다. 호이스팅을 사용하여 `node_modules`를 플랫했다.

pnpm은 호이스팅 대신 의존성 해결 전략의 대안책으로 내용 주소화 저장소(Content-addressable storage) 를 도입했다.

단순하게 파일 이름으로 해당 파일을 접근하는 것이 아니라 각각의 의존성 파일에 hash id를 부여하고 관리한다. 이 과정에서 중복되는 패키지는 동일한 hash id를 얻게 된다.

이 방법을 사용하면 홈 폴더(~/.pnpm-store/)의 전역 저장소에 패키지를 저장하는 중첩된 node_modules 폴더가 생성된다.

예를 들어 npm을 사용할 때 종속성을 사용하는 프로젝트가 100개인 경우, 해당 종속성의 사본 100개가 디스크에 저장된다. 하지만 pnpm을 사용하면 content-addressable 저장소에만 저장된다.

과정

  1. 다른 버전의 의존성에 의존하는 경우, 다른 파일만이 저장소에 추가된다.
  2. 예를 들어, 100개의 파일이 있고 새 버전이 그 중 하나만 변경되면, pnpm update 는 단일 변경에 대해 전체 파일이 복제되는 대신, 저장소에 1개의 새로운 파일만 추가한다.
  3. 모든 파일은 디스크 상에 한 위치에 저장된다. 패키지가 설치될 때 그 파일들은 단일 위치에서 하드링크되며 추가적인 디스크 공간을 소비하지 않는다.
  4. 따라서 프로젝트 간에 동일한 버전의 의존성을 공유할 수 있다!

결과적으로, 디스크 공간은 프로젝트와 의존성의 수에 비례하여 더 많이 절약되고 더 빠르게 설치할 수 있게 되는 것이다.

 

하드 링크
파일 시스템에서 사용되는 개념으로, 하나의 파일에 대해 여러 개의 이름(경로)을 가질 수 있도록 하는 방식. 하드링크를 통해 같은 파일의 여러 이름이 서로 다른 디렉토리에 존재하는 것처럼 보이게 된다. 이러한 링크들은 원본 파일과 동일한 데이터를 공유하며, 하드링크들 중 하나를 수정하면 나머지 하드링크들도 동일한 내용으로 수정되는 특징이 있다.

 

Boosting installation speed

pnpm은 3단계로 설치를 수행한다.

  1. Dependency resolution 의존성 해결
    필요한 모든 의존성을 식별하여 저장소로 가져온다.
  2. Directory structure calculation. 디렉토리 구조 계산
    의존성을 기반으로 `node_modules` 디렉토리 구조가 계산된다.
  3. Linking dependencies. 종속성 해결
    의존성 연결. 나머지 모든 의존성을 저장소에서 `node_modules`로 하드링크 한다.

Creating a non-flat node_modules directory

node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       ├── bar -> <store>/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo -> <store>/foo
    │       ├── bar -> ../../bar@1.0.0/node_modules/bar
    │       └── qar -> ../../qar@2.0.0/node_modules/qar
    └── qar@2.0.0
        └── node_modules
            └── qar -> <store>/qar

npm 또는 Yarn Classic을 사용하여 의존 항목을 설치할 때 프로젝트에 포함되지 않은 의존성(패키지)들에 대해서도 프로젝트의 소스 코드가 접근할 수 있는 상황이 발생할 수 있다. 왜냐하면 모든 패키지들이 공통된 모듈 디렉토리에 설치되기 때문에, 모듈 디렉토리 내의 모든 패키지들은 프로젝트의 어디서든 접근 가능하게 되는 것..!

pnpm은 symbolic link를 사용하여 프로젝트의 직접 의존성만 모듈 디렉토리의 루트에 추가한다. 이 레이아웃의 큰 장점은 실제로 의존성에 있는 패키지에만 액세스할 수 있다는 것.

peer dependency

pnpm의 가장 좋은 기능 중 하나는 한 프로젝트에서 패키지의 특정 버전이 항상 하나의 종속성 세트를 갖는다는 것이다. 하지만 이 규칙에는 한 가지 예외가 있다. 피어 의존성이 있는 패키지이다.
모노레포 사용시 `package.json`에 `peer dependency`를 명시하지 않으면 사용할 수 없는 pnpm의 엄격함이 있다.
➡️ 모노레포에 익숙하지 않은 사람이 만들어내는 실수를 방지하는 것.

 

pnpm과 모노레포로 구성한 프로젝트의 예시가 꽤 많아서 흥미로웠다. 추후에 pnpm은 왜 모노레포와 잘 맞는지, 모노레포는 무엇인지 등을 알아보겠다.

 

 


[우리는 하나다! 모노레포 with pnpm]

[Motivation / pnpm 공식 홈페이지]

[symlink된 node_modules 구조 / pnpm 공식 홈페이지]

[[번역] JavaScript 패키지 매니저 비교 - npm, Yarn 또는 pnpm?]

[패키지 매니저, 그것이 궁금하다.]

[[패키지 매니저] npm, yarn, pnpm, yarn-berry]

[모던 프론트엔드 프로젝트 구성 기법 - 모노레포 개념 편]

[패키지 관리자]

[패키지 매니저(Package Manager)란?]

[패키지 매니저(Package Manager)란?]

[[개발상식] npm과 yarn]

[알고 쓰자 package.json]

[Creating a package.json file]

[What are Dependencies in Programming]

[Yarn 대신 pnpm으로 넘어간 3가지 이유]

 ChatGPT 🤖