Back
Featured image of post 패키저매니저 돌아보기

패키저매니저 돌아보기

패키지매니저가 필요한 이유, 각각의 특징들

자바스크립트 모듈 시스템

우선 패키지 매니저를 살펴보기 전에, 왜 패키지매니저를 우리가 편히 사용하는지? 배경부터 살펴보도록 하겠습니다. module 초창기 자바스크립트 언어는 작게 시작되었기 때문에 웹 페이지에서 보조적인 상호작용 역할을 담당하는 모듈 시스템(현재의 CJS, ESM)은 따로 존재하지 않았습니다. 추후에 모듈 시스템이 탄생하게 되었는데, 이전까지는 <script> 태그를 사용하여 여러 자바스크립트 파일을 로드하고 사용했습니다.

모듈 시스템과 script 로드의 차이

그럼 script 태그를 사용해서 불러오는 방식으로 잘개 파일을 쪼개서 프로젝트를 구성하면 되지 않을까요? 일단 모듈과 스크립트 로드 방식에 대해서 차이점을 말씀드리도록 하겠습니다.

외부에서 작성된 여러 자바스크립트 파일을 불러오는 방식과 모듈 시스템의 차이는 독립적인 파일 스코프를 가지고 있는지에 대한 차이점이 있습니다. 태그를 사용하여 로드한 여러 자바스크립트 파일은 결국은 하나의 전역을 공유합니다. 전역 공간 오염을 최소화하기 위해 ESM, CJS 방식의 모듈 시스템이 등장하였으며, 각각의 차이는 다음과 같습니다. (이 글에서는 모듈에 대해서 깊게 다루지 않으니 이런 특정이 있구나..정도로 이해하시면 좋을 것 같습니다!)

  • CJS(CommonJS): Node.js 진영에서 채택한 모듈 시스템, 동기적으로 이루어지며 종속성 관계를 파악하지 않고 즉시 불어오기 때문에 불러온 즉시 모듈을 변경할 수 있다. (조건부 호출 가능, 불안정적, flat 구조)
  • ESM(ES Module): ES6에서 등장하였으며 비동기적으로 동작한다, 모듈 간 종속성 관계를 파악하고 불러온다. (조건부 호출이 불가능, 안정적, tree 구조)

패키지매니저

현재 자바스크립트 개발 생태계에서는 앞서 말한 ‘모듈’을 온라인 저장소(NPM Registry)에 게시하고 필요한 경우 편리하게 다운로드할 수 있는 여러 패키지 매니저가 존재합니다. 먼저 패키지의 정의를 살펴보도록 하겠습니다.

패키지

자바스크립트에서 패키지는 하나 이상의 모듈들의 집합으로, 별도의 이름으로 지정됩니다. 파일 시스템과 마찬가지로 하나의 패키지는 여러 하위 패키지로 구성될 수 있습니다. 패키지를 사용하면 재사용성을 높일 수 있으며, 프로그램이 해결하려는 문제를 빠르게 해결할 수 있습니다.

프로젝트 내 여러 패키지를 배치할 수 있으며, 이 과정에서 패키지 간 종속성이 발생할 수 있습니다. 하위 종속성이 없거나 간단한 경우에는 <script> 태그를 사용하여 포함할 수 있지만, 복잡한 경우에는 프로젝트 배포시 코드와 종속된 패키지를 함께 묶어주는 번들링 도구가 필요할 수 있습니다.

패키지매니저의 필요성

물론 이론적으로 패키지 매니저는 필수는 아니며, 사용자가 직접 원하는 패키지를 수동으로 관리할 수 있습니다. 하지만 패키지 매니저를 사용하지 않는다면, 아래와 같은 작업들을 사용자가 직접 관리해야 번거로움이 발생합니다.

  • 패키지 파일을 직접 다운로드하여 원하는 위치에 수동으로 배치
  • 올바른 패키지인지 검사 (취약점)
  • 설치한 패키지들의 하위 종속성에 대해 동일한 작업(업데이트 등) 필요

따라서 패키지 매니저를 사용하면 개발자들이 패키지 관리를 보다 원활하게 할 수 있도록 편의성을 제공할 수 있습니다.

의존성 타입

각 패키지매니저에서는 패키지 설치시 의존성 타입으로 분류할 수 있습니다. 기본적으로 dependency 타입으로 분류합니다.

dependency
일반적으로 프로젝트에 직접적으로 필요한 패키지들을 설치할 때, 이들은 기본적으로 dependency 타입으로 분류됩니다.

devDependency
프로젝트에는 필요하지만 실제 사용자들이 필요하지 않은 패키지들은 devDependency 타입으로 분류할 수 있습니다. 이러한 패키지에는 코드 컨벤션 관리를 위한 패키지나 테스트를 위한 패키지 등이 포함될 수 있습니다.

peerDependency
특정 패키지에 강한 의존성을 가진 경우, 호환성을 표시하기 위해 해당 프로젝트를 분류합니다. npm 3~6 버전까지는 peerDependency에 포함된 패키지까지 설치되지 않았으며 잘못된 종속성인 경우 경고 메시지가 표시되었습니다. npm 7부터는 기본적으로 peerDependency에 포함된 패키지도 함께 설치됩니다.

일반적으로, 별도의 라이브러리 위에 동작하고 싶은 패키지를 빌드할 때 해당 라이브러리나 플러그인을 명시합니다. 만약 peerDependency 내부 패키지들이 꼭 필요하지 않다면, peerDependenciesMeta 속성을 사용하여 충족되지 않아도 경고를 발생시키지 않을 수 있습니다.

lock file

각 패키지매니저에서는 lock 파일을 가지고 있습니다. 이 파일의 목적은 다른 협업자가 의존성 설치를 할 때 동일한 패키지를 생성하기 위함입니다. package.json 파일만으로는 상황에 따라 업데이트된 패키지를 다운받는 경우도 있습니다. (버전 차이로 인한 부수효과 방지) npm에서는 이전에 제공하지 않다가 추후에 package-lock.json 파일을 제공하고 있으며 yarn, pnpm에서도 기본적으로 제공하는 기능입니다.

패키지매니저의 조상님, npm

npm은 Node.js의 표준 패키지 매니저입니다. 초기에는 Node.js의 패키지 종속성을 관리하는 데 사용되었지만, 이후 프론트엔드 전반에서도 사용할 수 있게 되었습니다. package.json 파일에는 프로젝트에 사용되는 패키지가 정의되며, node_modules 디렉토리 내에는 실제 패키지에서 구성된 여러 모듈이 위치합니다.

특징

호이스팅
패키지 하위에는 수많은 의존 패키지들이 포함되어 있습니다. 이 중에는 동일한 패키지를 사용하는 패키지들도 있습니다. 이러한 경우 충돌을 해결해야 합니다.

foo
+-- blerg@1.2.5
+-- bar@1.2.3
|   +-- blerg@1.x (latest=1.3.7)
|   +-- baz@2.x
|   |   `-- quux@3.x
|   |       `-- bar@1.2.3 (cycle)
|   `-- asdf@*
`-- baz@1.2.3
    `-- quux@3.x
        `-- bar

예를 들어 blerg 패키지는 프로젝트에 직접적인 의존성을 갖습니다. (node_modules에 설치됨) 이때 각 패키지마다 node_modules 폴더를 순회하며, 이미 상위 위치에 설치된 경우에는 현재 위치에 다시 설치하지 않습니다. 동일한 패키지 의존성 충족이 다른 경우에만 해당 node_modules 위치에 설치합니다. 위와 같은 상황에서는 다음과 같이 패키지를 설치합니다.

foo
+-- node_modules
    +-- blerg (1.2.5) <---[A]
    +-- bar (1.2.3) <---[B]
    |   `-- node_modules
    |       +-- baz (2.0.2) <---[C]
    |       |   `-- node_modules
    |       |       `-- quux (3.2.0)
    |       `-- asdf (2.3.4)
    `-- baz (1.2.3) <---[D]
        `-- node_modules
            `-- quux (3.2.0) <---[E]
  • A: blerg 의 최신 버전은 1.3.7이지만, 특정 버전 종속이기에 해당 버전으로 설치한다. (1.2.5)
  • B: bar 에서 의존하고 있는 baz에서 다시 bar 패키지를 내부적으로 의존하고 있지만 버전이 동일하므로 상위에 설치된 패키지를 의존한다.
  • C: 상위에 baz 패키지가 존재하지만, 종속성 충족이 되지 않기 때문에(메이저 버전이 다름) 필요한 특정 버전으로 설치한다.
  • D: baz 패키지 내부의 quux 디렉터리가 비어져있는데, 이는 상위에 위치한 bar 복사본이 존재하기 때문에 충족되어 비어져있다.

clean install
npm install은 개별적으로 프로젝트를 설치할 때도 필요하지만, 원격 프로젝트를 초기 설치할 때에도 사용할 수 있습니다. 주의할 점은 기본 설치는 패키지 종속성 정보가 저장되어 있는 package-lock.json 파일을 수정한다는 것입니다. 그러나 npm ci는 기존 lock 파일을 확인하여 저장된 종속성을 기반으로 설치하기 때문에 이미 진행 중인 프로젝트를 설치하거나 빌드 및 배포하는 경우 npm ci 방식이 적합합니다.

Google과 Meta의 합작, Yarn

2016년에 출시된 Yarn은 기존 npm보다 더 빠른 속도와 offline mirror 라는 오프라인 환경을 지원하게 되었습니다. 패키지 설치 시 기본적으로 사용자 디렉터리 내부에 있는 글로벌 캐시에서 우선적으로 확인하기 때문에 빠른 속도를 보장할 수 있습니다.

  • 패키지 설치시 병렬 설치 방식으로 수행하여 성능을 향상
  • yarn.lock 파일과 자체 설치 알고리즘을 사용하여 패키지 설치시 비결정성 문제를 해결하고자 하였음 (이 부분은 npm5에서부터 pacakge-lock.json을 통해 해결)
  • 설치하지 않은 패키지는 tar 파일을 가져와 글로벌 캐시에 배치 → 동일한 패키지을 두 번 이상 다운로드 할 필요가 없도록 개선

Yarn Berry

Yarn Berry는 Yarn v2 이상의 모던한 환경을 말합니다. 이는 기존 yarn 환경에서 몇 가지 기능과 안정성이 추가되었습니다. (현재 Yarn 1.X 버전은 더 이상 유지보수되지 않고 있다고 합니다.) 아래 명령어로 Yarn Berry 버전으로 사용하실 수 있습니다.

yarn set version stable
yarn install

특징

Yarn Pnp
Yarn Berry에서 등장한 패키지 설치 전략은 기존 방식과 다르게 node_modules가 존재하지 않으며 별도의 로더파일(.pnp.cjs)을 통해 의존성 트리에 대한 모든 정보를 저장하고 .yarn/cache 디렉토리에 실제 패키지를 저장합니다. yarn PnP는 아래의 여러 문제를 해결하기 위해 도입되었습니다.

  • 실제 디스크에서 입출력하지 않고 Node.js 로더 파일을 활용
  • node_modules 구조에서 사용하던 호이스팅 방식을 사용하지 않음 → 정확한 의존성 정보
    (모든 패키지와 종속성 정보들을 보유하는 방식)

단점으로는.. PnP 모드 사용시 VSCode와 인텔리제이에서 TypeScript, ESLint, Prettier 기능을 사용하기 위해 별도의 SDK를 설치해야한다는 부분입니다.

yarn dlx @yarnpkg/sdks vscode

Zero Installs 이 기능은 기존의 offline mirror 방식보다 한층 발전된 기능입니다. 다른 개발자들과 협업할 때 버전 관리 시스템에서 즉시 yarn 환경을 다운로드 받을 수 있으므로 별도의 패키지 설치를 할 필요가 없어 시간과 비용을 절약할 수 있습니다. (이는 빌드와 배포 단계에서 더욱 이점으로 작용됩니다.)

협업으로 .gitignore 사용시 zero-installs 유무에 따라 다른 협업자와 공유되는 파일 목록에 차이가 있습니다.

#zero-installs 적용된 프로젝트
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

#zero-installs를 사용하지 않는 프로젝트
.pnp.*
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

효율적인 디스크 공간을 고려한 Pnpm

yarn과 npm에서 사용하는 node_modules 방식은 여러 프로젝트에서 필요한 패키지를 하드디스크에 설치하는 방식입니다. 예를 들어, 로컬에 다수의 React 프로젝트가 있으면 해당 프로젝트 수만큼 React 패키지 복사본이 사용자 하드디스크에 존재합니다. 이러한 문제를 해결하기 위해 pnpm이 등장했습니다.
또한 기존 Yarn 사용자들이 최대한 불편을 느끼지 않도록, Yarn에서 제공하는 PnP 기능을 사용할 수 있는 선택지를 제공합니다.

#.pnpmrc
node-linker=pnp
symlink=false

특징

pnpm-saving-disk-space pnpm motivation - saving disk space

Content-Addressable Storage (CAS)
pnpm은 컨텐츠 주소 지정 스토리지(CAS) 방식을 활용합니다. 각 패키지마다 해시 함수를 통해 암호화하여 각각의 패키지 주소 값을 갖습니다. 이는 파일의 이름이나 위치 기반이 아닌 파일 자체를 대상으로 하기 때문에, 공통된 패키지라면 하나의 주소 값을 부여합니다. 이를 통해 여러 프로젝트에서 동일한 패키지를 사용한다면 별도의 사본으로 추가시키는 것이 아니라 단일 파일로 관리할 수 있습니다. (✨ 디스크 공간 절약)

non-flat structure pnpm의 디렉토리 구조는 호이스팅 방식처럼 flat한 구조가 아닙니다. 심볼릭 링크 방식을 사용하여 non-flat 구조에서도 빠르게 접근할 수 있도록 설계되었습니다.

flat한 의존성 구조에 대한 문제점

  • 모듈이 의존하지 않는 패키지에 접근할 수 있다. (유령 의존성 문제)
  • 의존성 트리를 평탄화 시키는 호이스팅 과정에서 사용하는 알고리즘은 매우 복잡하다.

심볼릭 링크

#node_modules
.
├── @eslint
│   ├── eslintrc -> ../.pnpm/@eslint+eslintrc@2.1.2/node_modules/@eslint/eslintrc
│   └── js -> ../.pnpm/@eslint+js@8.47.0/node_modules/@eslint/js
├── @eslint-community
│   ├── eslint-utils -> ../.pnpm/@eslint-community+eslint-utils@4.4.0_eslint@8.47.0/node_modules/@eslint-community/eslint-utils
│   └── regexpp -> ../.pnpm/@eslint-community+regexpp@4.6.2/node_modules/@eslint-community/regexpp
├── @next
│   └── eslint-plugin-next -> ../.pnpm/@next+eslint-plugin-next@13.4.16/node_modules/@next/eslint-plugin-next
├── @rushstack
│   └── eslint-patch -> ../.pnpm/@rushstack+eslint-patch@1.3.3/node_modules/@rushstack/eslint-patch
├── @types
│   ├── node -> ../.pnpm/@types+node@20.5.0/node_modules/@types/node
│   ├── react -> ../.pnpm/@types+react@18.2.20/node_modules/@types/react
│   └── react-dom -> ../.pnpm/@types+react-dom@18.2.7/node_modules/@types/react-dom
├── @typescript-eslint
│   ├── eslint-plugin -> ../.pnpm/@typescript-eslint+eslint-plugin@6.4.0_ec2ucdo575yoxr55fjuy6wxey4/node_modules/
...
├── react-dom -> .pnpm/react-dom@18.2.0_react@18.2.0/node_modules/react-dom
└── typescript -> .pnpm/typescript@5.1.6/node_modules/typescript

앞서 말한대로 pnp에서는 심볼릭 링크(symlink) 방식의 도입을 통해 링크로만 참조하고 있는 형식상의 node_modules 디렉토리를 구성하는 방식을 취하고 있어 실제로 의존하고 있는 패키지만 접근하는 이점이 있습니다. 실제로 pnpm 프로젝트 환경에서 node_modules 디렉토리 구조를 살펴보면 겉으로는 일반적인 구조와 비슷해보이더라도 결국은 .pnpm 디렉토리의 실제 의존성 파일을 가리키는 링크로 이루어져 있습니다.

정리

이번 글에서는 왜 패키지매니저가 개발 경험을 향상시켰고, npm 이후에 등장한 여러 패키지매니저들이 어떤 강점을 내세우는지 간략하게 알아보았습니다. 각 패키지매니저가 지원하는 사항은 아래에 표로 간략하게 정리해보았습니다. 무조건적으로 특정 패키지매니저를 써야하는 것 보다는 상황에 맞게, 협업자들과의 트레이드오프를 통해 결정하는 것이 좋은 것 같습니다.

npm Yarn Yarn Berry pnpm
디스크 관리 copy copy copy CAS
의존성 관리 호이스팅 호이스팅 plug’n’play - symlink (기본)
lock file 지원 package-lock.json (verion 5) yarn.lock yarn.lock pnpm-lock.yaml
workspaces 지원 O (verion 7) O O O
zero-installs X X O X

저는 개인적인 경험으로 yarn-berry 사용시 sdk를 매번 설치해야하는 문제 때문에 당분간은 pnpm을 적극적으로 활용해볼까 합니다. ㅎ

참고 자료