Node.js의 패키지 매니저인 npm과 그 파생형의 비교

조경석·2023년 3월 12일
0

들어가기 전에 - npm

npm은 JavaScript 런타임 환경인 node.js용 패키지 관리자이다.

위 인용문처럼 쓰여있는 글은 인터넷에서 수백개도 더 찾을 수 있으니, 좀 더 하나하나 짚어보자.

JavaScript 런타임 환경

런타임(Runtime)은 말 그대로 컴퓨터가 이해할 수 있도록 컴파일된 소스 코드를 컴퓨터가 실행(Run) 동작중인 시간(Time)이다.
같은 방식으로 해석하면 런타임 환경은 런타임을 수행할 수 있는 환경으로 이해할 수 있다.

Javascript(이하 JS)는 특정 환경(ex : 웹 브라우저와 같은 응용프로그램)에서 동작하기 위한 스크립트 언어이고, 스크립트 언어는 별도의 컴파일 없이 실행 환경에서 소스 코드의 텍스트 자체를 파싱하여 분석, 실행한다.

즉 위 문단의 ex와 같이 웹 브라우저(정확히는 브라우저에 내장된 JS엔진)가 실행 환경≒런타임 환경이라고 거칠게 이해할 수 있다.

node.js

하지만 웹 개발에 관심이 있다면 node.js가 웹 브라우저는 아니라는 사실을 알고 있을 것이다.

node.js는 목적 자체가 웹 브라우저가 아닌 환경에서 JS 소스를 실행하는 것으로,
이로 위 인용문에서 "JavaScript 런타임 환경인 node.js"라고 설명한 것을 이해할 수 있다.

현재로서는 단순히 소스를 실행하는 것을 넘어 JS를 통해 서버를 구축하는 등 JS 자체가 독립적인 프로그램 개발에 사용될 수 있도록 확장시키는 역할을 수행하게 되었다.

패키지 관리자

npm을 설명하는 데 꼭 들어가는 문구가 하나 더 있다.

AAA에서 패키지를 관리하는 데 사용하는 BBB와 유사하다.
ex : Ubuntu 등 Devian 계열 리눅스 배포판에서 사용하는 apt와 유사하다.

이는 "적분은 미분의 반대, 미분은 적분의 반대."(심지어 틀린 말이다)라는 설명이나 마찬가지니, 패키지 개념에 대해 알아보자.

고전적인 의미의 패키지 관리자는 소프트웨어(응용 프로그램)에 대한 설치, 업데이트, 제거 등 관리 작업을 할 수 있는 도구이다.
현대 운영체제에서 사용하는 설치 파일(*.msi, *.app)를 실행하는 것과, 패키지 관리자를 통해 소프트웨어 패키지 파일을 실행하는것이 유사하다고 이해할 수 있다.

고전적인 CLI(Command-line Interface, ex : 터미널)에서 소프트웨어를 관리하기 위한 목적도 있지만, 현대에 와서도 프로그램의 설치, 실행 과정에서 필요한 다른 프로그램을 스크립트를 통해 설치하기 위해 사용된다.

이렇게 운영 체제에서 실행하는 소프트웨어를 관리하는 패키지 관리자는 보통 시스템 레벨 패키지 관리자라고 칭하며,
npm은 어플리케이션 레벨 패키지 관리자이다.

어플리케이션 레벨 패키지

위에서 node.js는 JS 실행 환경이라고 이해했고, 런타임 환경이라는 것 역시 JS엔진을 사용하는 소프트웨어(어플리케이션)이다.

npm은 이 node.js에서 사용하는 패키지를 관리하며, 이렇게 어플리케이션 수준에서 실행하는 데 필요한 패키지를 관리하는 어플리케이션 레벨 패키지 관리자라고 칭한다.

ES6에서 사용하는 모듈(import)이 이런 어플리케이션 레벨 패키지 관리 방식을 차용한 구조라고 설명할 수 있다.

이전 단락부터 "~하는 데 필요한"이라는 표현이 반복되는데, 이 부분이 패키지, 모듈을 포함한 개발 환경에서 사용되는 "의존성"의 개념이다.

의존성

dependency는 종속성, 의존성으로 번역되지만 이 글에서는 의존성으로 통일하도록 하겠다.
의존성 자체는 거칠게 요약하면 객체지향 개념에서 한 패키지의 클래스가 다른 패키지의 클래스를 참조하는 경우를 말한다.

현대적인 개발에서는 보통 특정 단위 기능이 많이 사용되는(일반적인) 기능인 경우, 새로 개발할 필요 없이 해당 단위 기능을 보편적으로 사용할 수 있도록 구현해둔 모듈을 참조하여 사용한다.
패키지 관리자에서는 이 단위 기능을 포함한 패키지들의 목록을 지정할 수 있으며, 참조해야 할 패키지들에 대해 의존성이 있다고 이해할 수 있다.

이 패키지 관리자의 기능으로 인해 git 등을 통해 소스를 배포할 때는 주요 기능의 코드가 아닌 보편적인 모듈의 소스까지 굳이 포함할 필요가 없어 최적화할 수 있으며,
새로운 어플리케이션을 개발하려는 경우 필요한 단위 기능을 모두 참조 가능하도록 준비한 상태로 개발을 시작할 수 있다.

물론 이로 인해 현대적인 어플리케이션에서 패키지 관리자로 설치된 모든 단위 기능을 이해하기는 불가능에 가까워졌고,
개별 패키지 자체도 의존하는 하위 모듈이 있어 모듈화된 패키지 아래에 모듈화된 패키지...가 반복되고, 결국 패키지 관리자 뿐만이 아닌, 의존성 관리자 등 추가적인 기능을 필요로 하게 되었다.

여기까지가 본문인 npm 파생 패키지 관리자의 차이, 장단점을 이해하는데 필요한 내용이다.

npm의 단점이었던 것

npm이 패키지 매니저라는 부분은 이해했고, 위 내용 중 패키지 매니저에 대한 설명의 일부는 npm이 가져온 혁신의 영향도 있어,
npm의 특징을 하나하나 따지기보다 아래 단락에서 알아볼 파생형들이 왜 나오게 되었는지를 파악하는게 더 직관적일 것 같다.

'하위 모듈' 처리

파생 패키지 매니저들이 가장 주창하는 npm과의 차별점이지만, npm의 버전에 따라 다르다.

npm1은 모든 의존성 모듈이 수평적 구조로 설치되었기 때문에 모든 패키지가 동일한 디렉터리에 설치되어 서로 충돌할 가능성이 있었다.

npm2는 특정 모듈 A가 모듈 B를 의존하는 경우, 수직적으로 모듈 B를 모듈 A 아래에 포함시킨다.
즉 모듈별로 의존하는 모든 '하위 모듈'을 포함하고 있으며, 이는 같은 모듈을 여러개를 설치하도록 하는 결과를 가져온다.

npm3는 모든 의존적인 모듈을 수평적으로 설치하는 정책으로 변경했지만, 새로(나중에) 설치한 모듈이 기존에 설치한 모듈의 다른 버전을 의존하는 경우 결국 '하위 모듈'로 그 버전을 포함시킨다.
즉 모듈을 설치하는 순서에 따라서 모듈의 설치 경로가 완전히 달라질 수 있다.

여기까지가 웹에서 쉽게 찾을 수 있는 내용이지만, 문제는 npm의 최신 버전은 무려 9.x다.
(npm은 '안정화 버전'을 따로 두지 않고, 최신 버전을 사용한다)
npm이 패키지 의존성 정책을 변경하지는 않았지만 결국 이 내용은 파생 패키지 매니저들이 발생하던 시점의 것으로,
현재로선 npm 자체적으로도 npm7에서 workspace 기능(yarn에서 차용)을 추가해 성능과 경로 문제를 해결했다.

의존 모듈 자동 실행

npm의 단점으로 설치된 모듈에 대한 검증 없이 자동으로 실행되는 보안 취약점이 있다는 글 역시 자주 검색되는데, 이 부분이 그 유명한 임의 코드 실행 취약점(ACE)이다.

npm의 패키지는 post-install 스크립트를 지정할 수 있으며, 이는 패키지를 설치(install)한 직후(post) 자동으로 실행할 기능을 정의한다.
기존의 npm에서는 이 스크립트가 악성 코드인 경우를 감지하는 기능이 없었으며, 이로 인해 대중적인 모듈의 배포자가 악의적 코드를 포함시키거나 보통 CLI로 사용되는 패키지 관리자의 특성을 악용해 모듈의 이름을 교묘하게 조작한 악성 모듈로 인한 공격 이슈가 있었다.

이는 2018년에 npm6으로 업데이트되면서 npm audit(전 Node Security Platform을 인수)으로 취약점 검사 기능이 추가되어 현행인 단점은 아니게 되었다.

패키지 작업의 직렬 처리

너무 웹 검색결과에 앵무새처럼 "npm은 직렬(순차)로 패키지 작업을 처리하는데 yarn은 아님"으로 가득해 이걸로 끝내긴 아쉬워, 좀 더 찾아보았다.

npm이 패키지 설치를 직렬로 처리하는 이유는 설치 프로세스의 안정성 검증을 위한 것이고, 또 매 설치마다 위 단락의 post-install 스크립트를 실행하기 위함이다.
원래 post-install 스크립트 자체가 패키지의 검증이나 '하위 모듈'에 대한 처리를 위함이므로, 이를 위한 처리를 항상 끝마치고 다음 패키지를 설치한다.

위 위 단락의 '하위 모듈'에 대한 처리 방식 역시 직렬 설치에 영상을 받으며, '설치한 순서에 따라'라고 설명되는 이유 역시 순차적으로 모듈의 버전과 위치를 처리하기 때문이다.

위 정책에 대한 npm의 생각은 바뀌지 않았는지, --parallel 명령 플래그나 여러 명령을 동시에 처리할 수 있게 해주는 패키지 npm-run-all의 run-p 명령을 포함해 쉘 명령이나 npm 스크립트를 병렬 처리하는 수많은 모듈이 있지만, npm 자체에서 패키지 작업을 병렬 처리하는 방법은 없었다.

대신, 병렬 설치를 지원하지 않는 이유 역시 위 정책이므로 서로 의존하지 않는 완전히 독립적인 프로젝트에 대한 병렬 설치는 당연히 가능하다.

파생 패키지 매니저

pnpm

Performant NPM(고성능)의 줄임말인데, 모든 패키지를 공용 저장소에 저장하고, 프로젝트에는 그 패키지의 경로를 저장해두어 성능을 향상시키는데 중점을 둔 패키지 매니저이다.

디스크 공간 효율

위 문단에서 설명한 대로 프로젝트 내에 패키지을 저장하지 않으므로 여러 프로젝트에서 하나의 패키지를 사용하는 경우 npm보다 디스크 공간 효율성이 높다고 말할 수 있다.

대신 파일 시스템상의 다른 경로를 운영 체제상의 경로 탐색에 의존해 찾게 되므로, 파일 환경(ex:네트워크 파일 시스템)이나 특정 운영 체제에서는 잠재적인 호환성 문제를 가지기도 한다.

패키지 병렬 설치

pnpm은 패키지를 병렬로 설치한다. 의존성 패키지가 많은 대규모 프로젝트인 경우 당연히 npm보다 설치 프로세스가 훨씬 빠르다.
대신 npm에서 중요시하는 설치 순서와 패키지 간 의존성에 대한 검사는 pnpm이 모두 직접 수행하므로, 해당 과정에서 오류가 발생한다면 직접 설치 스크립트를 수정해야 할 필요가 있다.

패키지를 설치하면서 작업하는 경우에도 불필요한 대기 시간을 최소화할 수 있지만, 역으로 시스템 리소스를 최대한 활용하여 설치 가능한 패키지를 병렬 설치하므로 오히려 설치를 진행하는 동안 다른 작업에 영향을 준다고도 할 수 있다.

패키지 메타데이터 캐싱

pnpm은 공용 저장소에 저장된 패키지들에 대한 메타데이터를 캐시로 저장해둔다.
만약 새로운 프로젝트에서 기존 공용 공간에 저장된 패키지를 필요로 하는 경우, pnpm은 기존에 설치된 패키지들의 메타데이터에서 먼저 탐색하여 불필요한 네트워크 리소스를 필요로 하지 않는다.
극단적인 예시긴 하지만 일부 폐쇄망이나 오프라인 환경에서 기존 패키지를 사용한 기능을 추가/테스트하는 경우에도 사용할 수 있는것이 pnpm이 장점이기도 하다.

Yarn

yarn classic이라고도 하는데, 이는 아래에서 설명할 yarn2의 등장 때문이다.
클래식이라는 이름에서 알 수 있듯이, 출범 당시에는 혁신적인 기능들을 자랑하고 npm의 고질적인 문제들을 해결했지만 현재 시점에서는 보편적이고 안정적인 기능을 보장하기 위해 사용하는 경향이 강해졌다.

패키지 병렬 설치

yarn 역시 pnpm과 같이 병렬 설치를 사용해 빠른 패키지 설치 속도를 자랑한다.
대신 하위 의존성 패키지가 있는 경우 npm3와 동일하게 '가능하면 수평적으로 설치하는' 방식을 사용하여, 설치 순서에 따른 문제가 생길 수 있는 잠재적인 위험 역시 따라오게 된다.
yarn은 이를 의존성 토폴로지 정렬(위상 정렬)을 통해 의존성이 적거나 충돌 가능성이 낮은 패키지를 먼저 설치하도록 해 해결하려고 시도했으며, 이를 위해 아래의 lockfile을 활용한다.

lockfile

yarn의 가장 특징적인 부분은 lockfile이라고 부르는 yarn.lock이라는 파일이다.(npm 5버전에서 package-lock.json 형식으로 해당 기능을 차용했다.)
lockfile 내에는 설치해야 하는 패키지의 목록과 정확한 버전이 모두 기록되어 있으며, 모든 패키지 설치 과정이 항상 동일하게 진행되도록 보장하는 역할을 수행한다.

이 덕분에 개발 협업 환경에서는 모든 개발자가 동일한 의존성 패키지 환경 내에서 작업할 수 있도록 하지만, 대신 lockfile과 실제 설치된 의존성 패키지의 상태를 항상 동일하게 관리하지 않으면 불필요한 충돌이 발생할 수 있다.

오프라인 모드

yarn은 yarn-offline-mirror 디렉토리에 패키지의 복사본을 저장할 수 있고, install --offline 명령을 통해 해당 복사본을 통한 설치가 가능하다.
이를 통해 인터넷에 연결되어 있지 않은 오프라인 환경이거나 온라인 패키지 저장소에 접근할 수 없는 상황에도 해당 캐시에서 패키지를 탐색할 수 있는 경우 패키지를 설치할 수 있다.

workspace

yarn은 디렉토리 하위의 개별 프로젝트를 패키지처럼 관리할 수 있는 workspace 기능을 제공한다.
이를 통해 하위 프로젝트에서 중복된 패키지를 사용해야 하는 경우 호이스팅(선언의 그것과 유사)을 통해 중복 설치할 필요 없이 의존성 패키지를 참조할 수 있도록 처리한다.

pnpm의 공용 저장소와 유사하지만, yarn은 모든 패키지를 한 공간(monorepo)에 저장하지는 않는다.
패키지를 관리하기 위한 정보를 package.json 내의 workspace 배열의 값으로 관리하며, 각 패키지는 각 프로젝트에 설치되고 모두 동시에 관리된다.

yarn berry

yarn2라고도 하며, 고전적인 npm에서 출발한 yarn의 한계였던 node_modules에 대한 의존을 개선하기 위해서 만들어졌다.
이로 인해 node_modules가 아예 없는 파격적인 형태로, PnP(Plug & Play) 개념을 적용하여 고전적인 형태의 패키지 관리자에서는 어쩔수 없던 모듈 저장소가 한없이 무거워지는 문제를 해결했다.

yarn 공식 페이지의 yarn2 출시 당시의 글이 대부분 삭제되어 yarn Berry인 정확한 이유가 무엇인지 웹에서 찾기가 어려운데,
1. 단순한 버전 2가 아닌 신선한(?) 버전임을 명시하기 위해 berry를 붙였다는 주장
2. 베리 송이(cluster)를 닮은 중앙 집중식 파일 트리 구조를 반영하기 위한 네이밍이라는 주장
대충 두가지 가설이 찾아지고, 둘 모두 딱히 신뢰도가 높진 않은 것 같다.

PnP(Plug & Play), Zero-Install

yarn berry는 패키지를 압축된 zip 파일로 관리한다.
이를 별도로 압축 해제하여 디렉토리에 저장하거나 패키지를 설치할 필요 없이 해당 zip 아카이브 자체가 의존성 패키지처럼 활용되며, 패키지가 필요할 때는 node.js상의 가상 파일 시스템에서 압축 해제하고 해당 시스템에서 바로 액세스한다.
이 때문에 node_modules을 사용하지 않으며, 패키지가 요청될 때만 가상 파일 시스템상에서 기능을 참조하므로 불필요한 디스크 용량과 패키지 색인을 개선했다.
yarn berry이 이 특징적인 기능으로 인해, 모노레포 프로젝트에 제일 최적화되어 있다고 볼 수 있다.

마무리

인터넷에서 검색할 수 있는 글들에서는 너무 같은 내용만 반복되는 것 같아, 기회다 싶어 최대한 보편적인 정리 글에서는 다루지 않는 부분까지 폭넓게 다뤄보았다.

장단점을 하나하나 나열해볼수록 결국 모든 node.js 패키지 매니저는 서로에게 영향을 주면서 발전했고, 가장 오래 된 npm조차도 버전이 바뀔 때마다 파생 패키지 매니저의 기능을 적극적으로 수용해왔다는걸 다시금 확인하게 되었다.
결국 패키지 매니저의 선택은 취향의 문제에 가깝고, 특정 패키지 매니저를 써야 할 이유는 존재할 수 있겠지만 역으로 특정 패키지 매니저를 쓰지 않아야 할 이유는 없겠다는 결론으로 마무리하고 싶다.

1개의 댓글

comment-user-thumbnail
2023년 3월 26일

안녕하세요, 제로베이스 프론트엔드스쿨 멘토입니다. 작성해주신 글 잘 읽었고, 앞으로의 더 나은 블로깅을 응원하는 마음에서 작은 의견을 남기고 갑니다 :)

  • 도입부에서 런타임 환경, node.js의 개념을 하나하나 꼼꼼하게 짚어가면서 패키지 매니저에 대해 설명하신 부분이 매우 인상깊었습니다. 개발자로서 이 내용을 정말 잘 이해하고있다는 느낌과 학습에 대한 열정이 느껴져서 좋았습니다.
  • 글이 전체적으로 작성자분만의 어투와 언어로 서술해주셨고, 전체적으로 본인의 생각과 이해를 중심으로 글이 작성되어있어 좋았습니다. 외부에서 가져온 내용을 그대로 쓴 느낌이 전혀 안들고 학습한 내용을 본인의 언어로 쭉 일목요연하게 잘 정리해주신 글인 것 같습니다.
  • npm 외에 다른 패키지 매니저가 등장했던 배경과, 패키지 매니저 각각에 대해서도 npm 대비 어떤것이 좋은지에 대해 정말 자세하게 분석하고 정리해신 것 같습니다.
  • 전체적으로 정말정말 잘 작성된 좋은 글이라고 생각합니다!! +_+ 저도 읽으면서 많이 배웠습니다.
  • 참고하신 링크가 있다면, 하단에 참고 링크를 적어서, 외부의 자료에서 가져온 내용과 내가 주도적으로 정리한 내용이 드러날 수 있도록 하면 좋을 것 같습니다.

감사합니다!

답글 달기