[Node.js] 패키지 매니저 npm과 yarn

이 글은 [Node.js 백엔드 개발자 되기]에서 발췌했습니다.
골든래빗 출판사

패키지 매니저는 애플리케이션에 의존적인 패키지를 관리(설치, 삭제, 업데이트)하는 프로그램입니다. 이를 사용하면 프로그램을 다운로드하고 설치하는 과정이 자동화되어, 사용자가 직접 복잡한 설정을 할 필요가 없습니다. 또한, 필요한 부가 프로그램이나 업데이트도 알아서 처리해줍니다. 이러한 특성 덕분에 사용자는 프로그램을 더 효과적으로 관리할 수 있으며, 프로그램 간에 발생할 수 있는 충돌이나 오류를 최소화할 수 있습니다.

대표적으로 npm과 yarn이 있습니다. npm은 용량 문제, 패키지 내려받기 속도 문제, 보안 문제를 가지고 있어서, 이런 한계를 개선한 yarn을 사용하기도 하고 사람에 따라서는 심볼릭 링크를 사용해서 npm의 문제를 해결한 pnpm을 사용하기도 합니다.

이 글에서는 npm와 yarn에 대해서 알아봅시다.

[JavaScript] 패키지 매니저 npm와 yarn

 

1. npm 소개

npm(node package manager)은 자바스크립트용 패키지 매니저입니다. 유저가 만든 패키지를 등록하는 저장소를 의미하기도 하고 CLI(Command Line Interface)를 의미 하기도 합니다. Node.js를 설치할 때 함께 설치됩니다.

npm은 패키지 저장소로서는 2022년 기준으로 세상에서 가장 많은 수의 패키지가 등록되어 있습니다. 그냥 많은 게 아니라 압도적으로 많습니다. 자바의 메이븐(Maven), .NET의 NuGet, 파이썬의 PyPI, PHP, 루비Ruby를 모두 합친 것보다 많은 패키지가 등록되어 있습니다.

 

▼ npm에 등록된 패키지 수 그래프(출처:http://www.modulecounts.com)

 

패키지 매니저는 프로젝트에 필요한 의존성 패키지를 관리하는 프로그램을 의미합니다. 의존성(Dependency) 패키지는 해당 프로젝트를 실행하는 데 꼭 필요한 라이브러리와 모듈들입니다.

예를 들어 《Node.js 백엔드 개발자 되기》의 express-server 예제에서 npm으로 익스프레스 라이브러리를 설치하고 사용했습니다. express-server는 익스프레스 라이브러리가 없으면, 동작하지 않기에 익스프레스에 의존성이 있다고 말할 수 있습니다. npm은 이런 의존성 패키지를 잘 관리할 목적으로 만들었습니다. 단순하게 의존성 패키지 리스트만 잘 관리하면 될 것 같지만, npm은 이보다 더 많은 기능을 제공합니다. 이제부터 차근차근 알아봅시다.

 

2. 패키지와 모듈

Node.js에서 패키지는 package.json으로 정의한 파일 또는 디렉터리를 의미합니다. 패키지에 는 package.json이 꼭 포함됩니다. 다음에 정리한 것들 모두가 패키지가 될 수 있습니다.

  1. package.json 파일이 있는 디렉터리
  2. 1번을 압축한 파일
  3. 2번을 내려받을 수 있는 URL 주소
  4. 3번 정보를 가지고 npm 저장소에 <패키지명>@<버전>으로 등록된 것
  5. 4번을 가리키는 <패키지명>@<태그>
  6. <패키지명>만 있는 경우는 5번에서 latest 태그를 가리킴
  7. 1번을 결과로 주는 깃(git) URL

결과적으로는 package.json으로 정의한 코드 뭉치가 바로 패키지인 겁니다. 그럼 모듈은 어떻게 다를까요? 모듈은 [node_modules] 디렉터리 아래에 있는 파일 또는 디렉터리를 말합니다. [node_modules]에 있는 파일이나 디렉터리는 require( ) 함수로 읽을 수 있습니다.

 

💡Note: CommonJS는 브라우저 뿐아니라 서버 애플리케이션에서도 모듈 기능을 제공하기 위해 나온 모듈 규약입니다. ES6가 나오기 전에 많이 사용되었고, 특히 Node.js의 기본값으로 사용되고 있습니다. ES module는 ES6의 자바스크립트 모듈의 표준입니다. 패키지를 임포트할 때 CommonJS(CJS)는 require( ) 함수로, ES module(ESM)은 import로 임포트합니다. Node.js는 기본적으로 CommonJS 문법을 사용합니다. package.json에 type 속성을 module로 설정하면 ESM을 사용할 수 있습니다. Node.js에서 ESM을 사용하면 프론트엔드의 코드를 백엔드에서도 쉽게 가져가 사용할 수 있는 장점이 있습니다.

 

[node_modules]에는 npm install 명령으로 설치한 패키지들이 저장됩니다. 즉 모든 패키지는 모듈입니다. 또한 npm에 등록하지 않아도 패키지 포맷만 맞다면 npm install로 설치할 수 있습니다.

패키지로 만들면 npm 레지스트리*에 등록할 수 있습니다. 패키지로 만든 코드들은 간단하게 다른 곳에서 설치해 사용이 가능하므로 사설 npm 레지스트리에 공통으로 사용하는 패키지를 배포해서 사용하기도 합니다.

* https://www.npmjs.com 또는 사설로 설치한 자바스크립트 패키지 저장소

 

사실 require( ) 함수를 사용할 때 단순히 현재 디렉터리의 [node_modules]만 읽는 것은 아닙니다. module.paths에 있는 경로를 따라서 모듈을 찾습니다. 정말 그런지 터미널에서 확인하겠습니다.

[sample-package] 디렉터리를 만들고 node 명령을 실행해, 사용자가 커맨드를 입력하면 시스템이 값을 반환하는 환경인 Node.js REPL(Read-Eval-Print-Loop)에 들어간 후 module.paths를 실행합시다.

 

$ mkdir chapter4
$ cd chapter4
$ mkdir sample-package
$ cd sample-package
$ node
# 아래부터 Node.js의 REPL입니다.2 > module.paths
[
  '/Users/gyus/VSCode/jsbackend/chapter4/sample-package/repl/node_modules',
  '/Users/gyus/VSCode/jsbackend/chapter4/sample-package/node_modules',
  '/Users/gyus/VSCode/jsbackend/chapter4/node_modules',
  '/Users/gyus/VSCode/jsbackend/node_modules',
  '/Users/gyus/VSCode/node_modules',
  '/Users/gyus/node_modules',
  '/Users/node_modules',
  '/node_modules',
  '/Users/gyus/.node_modules',
  '/Users/gyus/.node_libraries',
  '/Users/gyus/.nvm/versions/node/v16.14.0/lib/node'
]

 

출력 결과에 [chapter4/node_modules]가 있습니다만, 사실 만들지도 않은 디렉터리입니다(아예 생성도 되지 않은 디렉터리인데 패키지 매니저는 있다고 가정하고 설정한 경로입니다). 다른 [node_modules] 디렉터리들도 마찬가지입니다. 실제 존재하지도 않은 경로를 계속 타고 거슬러 올라가면서 [node_modules]가 있는지 검사합니다. 상위 디렉터리에 있는 패키지를 계속 타고 올라가면서 [node_modules]를 확인하면서 굉장히 많은 I/O를 수행합니다. 이것은 require( ) 함수가 무거워지는 원인이 됩니다. 이제 이 문제점을 해결한 yarn 프로젝트를 알아봅시다.

 

3. 패키지 설치, 업데이트, 삭제

npm install, npm update, npm uninstall 명령어를 간단히 알아보겠습니다.

 

3.1 패키지 설치하기

npm을 사용해 가장 많이 사용하는 기능은 패키지 설치입니다. npm install 명령으로 패키지를 설치할 수 있습니다.

 

 

npm install 대신 npm i 또는 npm add를 사용할 수도 있습니다. 단순히 별칭alias 기능이므로 셋 중에서 뭘 사용해도 결과는 같습니다.

앞서 말씀 드렸다시피 npm은 세계 최대의 패키지 저장소입니다. 무려 180만 개가 넘는 패키지가 있어서 패키지명이 중복되는 문제가 있습니다. 패키지 스코프를 사용하면 이 문제를 피할 수 있습니다. 패키지 스코프는 일종의 네임스페이스입니다. @로 이름이 시작하는데 가장 유명한 스코프로 @types가 있습니다. 타입스크립트의 타입 정의 패키지는 거의 대부분이 @types 스코프로 시작합니다. 스코프를 사용해서 패키지명이 같더라도 안전하게 사용할 수 있게 되었습니다. 예를 들어 test라는 패키지가 있다고 하겠습니다. @cat 스코프 밑에 test를 넣고 @rabbit 스코프 밑에 test를 각각 넣으면 패키지명은 같지만 스코프가 다릅니다. 결국 @cat/test, @rabbit/test로 각각 사용할 수 있기 때문에 충돌이 발생하지 않습니다.

name은 패키지명입니다. 패키지명 외에도 태그, 버전, 버전 범위가 필요합니다. 없는 경우는 latest 태그를 기본값으로 사용합니다. npm install에는 사용하는 옵션을 다음 표에 정리해두었습니다.

 

▼ npm install의 옵션 플래그

 

3.2 패키지 업데이트하기

패키지 업데이트는 설치보다는 명령어가 단순합니다. 별칭으로 npm up, npm upgrade를 사용할 수 있습니다.

 

$ npm update [-g] [패키지명1, 패키지명2...패키지명N] $ npm up
$ npm upgrade

 

-g 옵션은 install과 마찬가지로 node가 설치되어 있는 디렉터리의 의존성 패키지를 업데이트할 때 사용합니다.

패키지명이 없으면 package.json에 정의한 의존성 패키지들 전부를 업데이트합니다. package.json에 의존성 패키지 버전을 적을 때 앞에 ^ 또는 ~ 기호를 사용할 수 있습니다. 의존성 버전 설정에 캐럿을 붙이면 메이저 버전 이외에는 모두 업데이트합니다.

예를 들어 ^1.0.0은 1.0.0보다 크거나 같고, 2.0.0보다는 작은 버전을 의미합니다. 즉 1.X.X 버전은 모두 업데이트하겠다는 의미입니다. 다만 1.0.0 미만인 때는 API 변경이 수시로 일어나므로 0.1.0 버전과 0.2.0 버전은 호환이 안 될 수 있습니다. 그래서 이 경우는 예외를 두게 됩니다. ^0.1.0은 0.1.0보다 크거나 같고 0.2.0보다 작은 버전까지 업데이트합니다. 즉 0.1.X 버전까지 업데이트합니다.

물결표인 ~는 틸드라고 읽으며, 현재 지정한 버전의 마지막 자리 내 범위만 자동으로 업데이트합니다. 예를 들어보겠습니다.

  • ~0.0.1은 0.0.1보다 크거나 같고 0.1.0보다는 작은 범위에서만 업데이트합니다. 즉 0.0.X 버전까지만 업데이트합니다.
  • ~0.3.1이라면 0.3.1보다 크거나 같고 0.4.0보다는 작은 버전까지 업데이트합니다.
  • ~1은 1.0.0부터 1.x.x까지 업데이트합니다.

시맨틱 버전의 하위 의존성 여부는 해당 패키지를 만드는 사람의 마음대로 하는 경우도 많으므로 의존성 업데이트를 한 다음에는 꼼꼼하게 테스트해 문제가 없는지 확인해야 합니다.

 

▼ 의존성 버전 정보 정의에 따른 업데이트 범위

 

3.3 설치한 패키지 확인하기

설치한 패키지의 리스트는 ls 명령으로 확인할 수 있습니다. 별칭으로 list, la, ll을 대신 사용할 수 있습니다.

 

$ npm ls [@스코프/] 패키지명 $ npm list
$ npm la
$ npm ll

 

4. npm의 대안 yarn

npm 사용법을 익히면 npm을 사용한 패키지 관리가 쉬운 일이 아니라는 생각이 들겁니다. npm은 용량 문제, 패키지 내려받기 속도 문제, 보안 문제를 갖고 있습니다. node_module을 사용하는 방식이 아닌 다른 방법으로 패키지 관리를 할 수는 없을까요? yarn은 이러한 문제들을 해결할 목적으로 2016년에 페이스북에서 만든 패키지 관리 프로그램입니다.

yarn은 버전 1과 yarn berry라고 부르는 버전 2가 있습니다. 버전 1은 npm과 거의 유사하며 패키지 설치가 조금 더 빠른 정도입니다. 반면 버전 2는 PnP(Plug’n’Play) 전략을 사용하며 [node_modules]를 사용하지 않고 의존성 패키지를 관리합니다. PnP 전략은 패키지를 적절한 위치에 꽂으면(Plug) 바로 실행(Play)하도록 단순화하는 데 있습니다. 이를 위해 [node_modules] 디렉터리를 사용하지 않고, 의존성 찾기는 .pnp.cjs에 정리하고, 실제 의존성 패키지 파일은 압축 파일의 형태로 [.yarn] 디렉터리 아래의 [cache] 디렉터리에 저장합니다. [cache] 디렉터리 안에 있는 패키지들은 트리 구조가 아니라 모두 끌어올림(hoisting)되어 평평하게 저장됩니다.

PnP 전략을 사용해 의존성 패키지를 코드 저장소에 바로 저장하면, 당연한 말이지만 추후에 서버 배포 시 패키지 설치를 하지 않아도 됩니다. 이것을 제로 설치(zero install)라고 합니다. 제로 설치를 하면, 서버에 소스 코드를 배포할 때 패키지도 같이 설치가 됩니다. 그러면 패키지 설치 시점에 따라 버전이 달라져서 버그가 발생하는 일도 없어지게 됩니다.

PnP를 사용함으로써 얻는 이점이 또 하나 있는데, 그것은 서버의 기동 속도가 빨라진다는 겁니다. 애플리케이션이 동작하려면 require로 불러오는 의존성 패키지가 어디 있는지 다 파악을 해야 해서 많은 파일 탐색 동작이 일어납니다. PnP를 사용하는 yarn2에서는 기동할 때 의존성 패키지가 어디에 있는지 파일시스템을 순회하면서 찾을 필요가 없으니 의존성 패키지 위치를 찾는 시간 만큼 빨리 기동됩니다.

 

▼ npm과 yarn 명령어 비교

 

5. 마무리

npm이 없었다면 자바스크립트 생태계는 지금처럼 커지지 못했을 겁니다. npm은 개발자들에게 높은 자유도를 제공해 대중화에 성공했지만, 반대급부로 개발과 배포 테스트에 어려움을 주고 있습니다. npm의 대안으로 yarn2가 있으며 yarn의 PnP 전략이 대부분의 경우 유효하지만 아직까지 지원하지 않는 프로젝트들이 있다는 점에 유의해주세요.

yarn 이외에도 pnpm이라는 패키지 매니저도 있습니다. pnpm11은 심볼릭 링크를 사용해 패키지 중복 설치를 없애는 전략을 사용합니다. yarn에는 PnPify12 라이브러리를 제공해 PnP를 지원하지 않는 라이브러리를 수정하는 라이브러리도 있습니다.

개인적으로는 yarn2를 추천드립니다. 제로 인스톨 전략과 함께 사용하면 더욱 효과적이라 생각합니다. 실제로 사용하는 회사도 점점 늘어나는 추세입니다. 다만 사용하고자 하는 오픈 소스가 yarn에서 지원하지 않는 기능을 사용하고 있을 수 있습니다. 이런 때에는 주의 및 노력이 필요합니다.

박승규


아직도 개발이 재미 있는 15년차 천상 개발자입니다. 웹 개발, 게임 백엔드 개발, 플랫폼 및 인프라 개발 등 다양한 영역을 경험했습니다. 현재는 카카오엔터테인먼트에서 백엔드 개발자로 일합니다.


현) 카카오엔터테인먼트 페이지 서비스 개발팀
전) 트리노드 (포코팡, 포코포코) 서버 개발자
전) NHN Japan 플랫폼 개발팀

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
상호명 : 골든래빗 주식회사
(04051) 서울특별시 마포구 양화로 186, 5층 512호, 514호 (동교동, LC타워)
TEL : 0505-398-0505 / FAX : 0505-537-0505
대표이사 : 최현우
사업자등록번호 : 475-87-01581
통신판매업신고 : 2023-서울마포-2391호
master@goldenrabbit.co.kr
개인정보처리방침
배송/반품/환불/교환 안내