패키지 매니저
패키지란?
- 설치, 업데이트, 삭제, 의존성 관리가 가능한 코드 묶음 배포
- 버전이 있고, 의존성이 정의되어 있으며, 패키지 매니저가 수명주기를 관리하는 코드 묶음
- 단순히 파일들을 담은 폴더가 아니라 메타데이터를 가진 배포 단위를 뜻함
라이브러리와 다르다. 라이브러리가 코드의 묶음이라면 패키지는 해당 라이브러리 코드의 배포를 위해 사용되는 코드 묶음
패키지가 되기 위한 필수 요소
{
"name": "axios",
"version": "1.7.0",
"dependencies": {
"follow-redirects": "^1.15.6"
}
}
- 이름(name) - 식별자
- 버전(version) - 호환성 관리
- 의존성(dependencies) - 자동 설치 대상
- 엔트리 포인트(main / export) - 사용 진입점
의존성
- 여러 패키지들이 동작하기 위해 다른 패키지가 필요한 경우가 많음
- 기존 패키지를 동작시키기 위해 필요한 다른 패키지를 dependency라고 부름
- 패키지를 설치해서 사용하고자 한다면 dependencies에 해당하는 패키지를 전부 설치해야 함
- 의존성 패키지를 설치하는 도중 의존성 패키지의 의존성 패키지를 설치하는 경우가 발생할 수 있음
- 이런 상황이 끊임없이 이어질 경우 사용자가 수동으로 패키지를 관리하기가 굉장히 어려워짐
- 이런 경우를 dependency hell이라고 부름
- 모든 패키지가 자신의 dependecies 정보를 가지게 한다면, 패키지 매니저를 이용해서 쉽게 설치할 수 있음
패키지 매니저란?
- 패키지를 다루는 작업을 편리하고 안전하게 수행하기 위해 사용되는 툴
- 패키지 설치, 업데이트, 수정, 삭제를 명령어만으로 수행 가능
- Software repository로부터 패키지를 찾고, 다운하고, 설치하고 업데이트 함
Software repository?
- 패키지를 저장하고 관리하는 저장소
- 커뮤니티에 기여하는 것을 목적으로 다른 사용자들을 위해 패키지를 등록
- 성능 문제 및 에러 대처를 위해 분리되어 있는 경우도 있지만 각 저장소가 동일한 기능을 수행
npm, yarn, pnpm
위 3가지는 Javascript의 주요 패키지 매니저이다.
- 사실상 모든 패키지 매니저의 기능은 거의 동일
- 설치 속도, 사용량, 기존 워크플로우와 결합되는 방식 등 외적인 요구 사항을 기준으로 패키지 매니저를 선택하는 시대가 도래한 것
npm
등장배경
- 야후에서 일하던 한 개발자가 개발 과정의 일환으로 여러 코드를 패키지로 묶는 것에 익숙
- JS 코드 역시 다른 사람들에게 공유하기 위해 하나의 번들로 묶어서 만들고 배포
- npm이 등장하기 전에는 CDN 링크로 직접 사용
- 정적 리소스를 전 세계에 분산된 서버에서 빠르게 내려주는 배포 방식
- 파일을 업로드하고 링크로 사용하면 사용자와 가까운 서버에서 전달하는 방식
<script src="https://cdn.example.com/jquery.min.js"></script>
- 버전 고정 불가
- CDN 링크에 파일이 바뀌면 바뀐 코드가 내 서비스에서 실행
- 의존성 관리 불가능
- 다른 라이브러리에 의존하는 라이브러리가 있다면 직접 순서 맞춰서 로드
- 충돌 및 관리 어려움
<script src="a.js"></script>
<script src="b.js"></script>
그렇기 때문에 버전 및 의존성 관리가 용이하도록 npm이 탄생한 것
npm의 주요 기능
단순한 패키지 매니저가 아니라 다양한 기능을 제공함으로써 생산성을 높임
- 패키지 설치 및 관리
- npm이 가진 가장 기본적인 기능
- package.json의 의존성 부분에 작성된 패키지를 node_modules에 설치해서 프로젝트에서 사용할 수 있도록 해줌
- 패키지 배포 및 공유
- 개발자가 만든 패키지를 업로드할 수 있는 레지스트리 서비스
- 스크립트 실행
- package.json의 scripts 필드를 사용하면 원하는 스크립트 실행 가능
- npmjs.com
- npm에 업로드된 패키지에 대한 정보를 시각화
💡 npm 유용한 사이트
- npm에 업로드된 패키지를 검색하여 해당 패키지의 크기를 확인할 수 있는 사이트
- 패키지 수가 방대한 만큼 서로 비슷한 기능인 패키지 중에 고민 되는 경우가 있음
- 다양한 패키지들의 현재 상태와 함께 다운로드 횟수를 비교할 수 있는 사이트
- 사용자가 많다고 좋은 것은 아니지만, 어느 정도 안정성을 가지는 지표로 사용 가능
package.json
프로젝트의 의존성, 스크립트, 각종 설정을 정의하는 역할
프로젝트의 구조와 특성을 파악할 수 있지만 초기 설정 이후 자세히 들여다보지 않는 경우가 대다수
속성
- name - 프로젝트 이름
- npm에 업로드하지 않는 웹 서비스의 경우 필수 아님
- npm에 업로드하여 다른 사용자들이 사용할 수 있게 할 경우 고유 명칭을 지정해야 함
- version - 버전
- name과 version은 항상 npm 레지스트리 내부에서 고유해야 함
- 하나의 name에는 중복되는 version이 존재할 수 없음
- 해당 프로젝트의 버전을 명시
- 배포 목적이 아니라면 필수 아님
- type - 프로젝트 타입
- module, commonjs 둘 중 하나 입력 가능
- 아무것도 입력하지 않으면 commonjs가 기본값
- module 형식이라면 import 문 사용
- commonjs 형식이라면 require 문 사용
- description
- 프로젝트의 설명
- npm info <패키지명> 으로 설명 확인 가능
- 프로젝트에서 자주 실행되는 명령어들을 단축 명령어로 정의
- dev, build, lint 등
- vite 서버를 실행시킬 때 npm run dev로 지정해주지 않는다면?
- 실제 경로에 파일을 실행시켜서 사용해야 함(매우 불편)
- 프로덕션 환경에서 필요한 패키지들
- 설치한 패키지가 실행되는데 필요한 외부 패키지 및 라이브러리를 정의하는 필드, 프로젝트가 의존하는 패키지들과 해당 버전 범위가 명시
- 내 프로젝트에서 어떤 패키지들이 필요한지 작성되어 있는 곳
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.11.0"
},
- 개발 중에만 필요한 패키지들
- 빌드 이후에는 필요하지 않은 패키지들이 명시
- eslint, prettier 등
react나 next를 설치하면 자동적으로 package.json이 작성이 되기 때문에 잘 모르고 넘어가는 경우가 많지만, 속성들이 어떤 역할을 하는지 정도는 알고 있자.
node_modules
npm install 명령어를 입력하면 node_modules 폴더에 패키지가 설치된다.
npm은 의존성을 효과적으로 관리하고, 필요한 패키지를 최적화된 형태로 프로젝트에 설치하기 위해 node_modules 폴더를 만들어 사용한다.
하지만 이것도 설치만 하고 들여다보지 않는 경우가 대다수이다.
node_modules의 역할
- 의존성 관리와 모듈 경로 해결에서 중요한 역할
- 의존성 트리를 분석하고, 필요한 의존성만 설치하는 과정을 거쳐 필요한 패키지와 모듈이 node_modules 폴더에 저장
node_modules의 주요 기능과 특징
- 의존성 관리
- package.json에 명시된 목록을 기준으로 필요한 패키지들을 설치해서 관리
- 경로 해결
- require나 import로 모듈을 가져올 떄 node_modules 폴더는 해당 모듈을 검색하는 주요 경로
- 모듈을 찾기 위해 node_modules부터 필요한 위치까지 폴더 구조를 따라가며 탐색
import { useNavigate } from 'react-router-dom';
- 이러한 import 문이 있을 때, react-router-dom만 경로로 입력해도, node_modules 내부에 react-router-dom 위치로 탐색하여 import 실행
- 네임스페이스 관리
- 의존성 트리 내 여러 패키지가 동일한 이름의 모듈을 필요로 하는 경우가 많음
- 중복 모듈이 서로 영향을 미치지 않도록 각 패키지가 독립적으로 사용할 수 있게 네임스페이스 관리
- 서로 다른 두 패키지가 각 다른 버전의 모듈을 요구할 경우 node_modules는 하위 폴더로 분리해서 설치함으로써 충돌을 방지
node_modules 구조
node_modules/
- js-tokens
- loose-envify
- react
- - js-tokens
- react-dom
- - js-tokens
- 의존성 패키지들이 중복될 경우 해당 폴더 내부에 설치하는 것이 아닌 동일한 위치에 설치
- 같은 레벨(동일한 위치)에 일괄적으로 설치하는 것을 ‘평탄화’라고 함
- 2개의 패키지 설치 ⇒ C 패키지라는 중복 의존성 발생
- node_modules는 단순히 의존성을 설치하고 관리하는 것만이 아님
- 빌드 도구와 패키지 관리에서 성능 최적화를 위해 활용됨
- 이러한 구조 덕분에 개발자는 더 효율적으로 작업이 가능
- node_modules 폴더의 작동 방식을 이해해야 의존성 관리에 있어 더 높은 수준의 통제 가능
하지만 실제로 패키지를 만드는 사람이 아니라면 실제 의존성 관리보다는 이러한 역할과 원리를 이해하고 넘어가면 됨
npm의 문제점
중요한 도구로 개발자들에게 자리매김 했지만, 프로젝트 규모가 커지고 다양한 환경에 대응해야 하는 상황들이 생기면서 npm 만으로는 해결하기 어려운 몇 가지 한계가 나타나기 시작했다.
유령 의존성
- 평탄화 작업으로 인해 디스크 공간 절약 및 불필요한 의존성 depth 감소라는 목표는 달성
- 개발자 입장에서는 설치하지 않은 패키지가 본인 프로젝트에서 사용이 가능한 것
package.json:
{
"dependencies": {
"a": "1.0.0",
"b": "1.0.0"
}
}
a 패키지 → c@1.0.0 의존
b 패키지 → c@1.0.0 의존
설치 결과:
node_modules/
├── a/
├── b/
└── c/ ← 평탄화됨
app.tsx:
import something from 'c' => 사용 가능! why? node_modules에 있기 때문에 but 위험함(직접 설치하지 않은 패키지)
- 실제로 설치하지 않은 패키지가 존재하여 이를 사용하다가 a와 b 패키지가 업데이트 되면서 c 패키지에 대한 의존성을 제거하면 사용이 불가해짐
import something from 'c'
```
### a와 b가 c 의존성 제거하면?
```
a@2.0.0 업데이트 → c 의존성 제거
b@2.0.0 업데이트 → c 의존성 제거
npm install 후:
node_modules/
├── a/ (2.0.0)
├── b/ (2.0.0)
└── c/ ← 사라짐!
너무 거대한 node_modules
- 많은 패키지를 설치하면서 원하는 기능 뿐만 아니라 패키지에 내장된 다른 수많은 파일들이 함께 설치됨
- 불필요한 디스크 공간 사용
- 빌드 시간 증가
- node_modules가 클수록 빌드 시간도 증가
- npm 주요 명령어들이 실행되는 시간 증가
lock 파일
- package-lock.json이란?
- npm이 특정 시점의 package.json을 기준으로 의존성 트리를 저장해서 의존성 업데이트가 발생하더라도 동일한 설치 환경을 보장하기 위해 생성된 파일
- 여러 개발자와 협업을 할 경우 동일한 환경이 유지되는 것은 매우 중요
npm 탓은 아니지만 문제점들이 있고, 이러한 문제들을 해결하기 위해 yarn과 pnpm이 탄생한 것이다.
다른 패키지 관리자들이 왜 탄생했는지, yarn은 무엇이고, pnpm은 무엇인지, npm과 비교하면 어떤 장점을 가지고 있는지 알아야 앞으로 프로젝트를 하면서 어떤 패키지 관리자를 선택해야 할지 명확하게 결정할 수 있을 것이다.
yarn
등장 배경
- 2016년 Facebook에서 npm의 성능과 안정성 문제 해결을 위해 개발
핵심 개념
- 병렬 설치 - 여러 패키지를 동시에 설치
- 오프라인 캐시 - 한 번 설치한 패키지는 로컬에 저장
- 체크섬 검증 - 패키지 무결성 보장
문제 해결
- 유령 의존성
- 평탄화 형식을 사용하기 때문에 여전히 유령 의존성 존재
- 그 유명한 Yarn Berry
- PnP 모드 도입
- node_modules 자체를 제거
- .pnp.cjs 파일에 모든 패키지의 위치와 의존성 관계를 저장
- 의존성 관계에 작성된 패키지만 사용이 가능하기 때문에 유령 의존성 해결
- 패키지들은 .yarn/cache 디렉토리에 저장함
- .zip 파일로 압축한 상태 그대로 저장
- .pnp.cjs에서 맵핑 정보만을 저장하고 import문이 실행됐을 때 yarn은 zip파일 압축을 해제하지 않고 직접 읽어서 실행
- 거대한 node_modules
# 병렬 다운로드 (npm은 순차적)
npm install: A → B → C → D (4초)
yarn install: A + B + C + D (1초)
# 캐시 활용
첫 설치: yarn install (5초)
두 번째: yarn install (0.5초) ← 캐시에서 복원
- 병렬 설치 및 캐시 활용으로 성능 최적화
- Yarn Berry에서는 zip 파일 그대로 설치하여 3배 이상의 공간 절약
pnpm
등장 배경
- Performant npm의 약자로 npm과 yarn의 모든 문제를 해결하기 위해 탄생
핵심 개념
- 심볼릭 링크 및 하드 링크
- 콘텐츠 주소 저장소(CAS)
문제 해결
- 유령 의존성
node_modules/
├── .pnpm/ ← 실제 패키지들 (하드 링크)
│ ├── a@1.0.0/
│ │ └── node_modules/
│ │ ├── a/ ← 실제 파일
│ │ └── c/ ← 심볼릭 링크 → ../../c@1.0.0/node_modules/c
│ ├── b@1.0.0/
│ │ └── node_modules/
│ │ ├── b/
│ │ └── c/ ← 심볼릭 링크
│ └── c@1.0.0/
│ └── node_modules/
│ └── c/ ← 실제 파일
├── a/ ← 심볼릭 링크 → .pnpm/a@1.0.0/node_modules/a
└── b/ ← 심볼릭 링크 → .pnpm/b@1.0.0/node_modules/b
- 최상위 node_modules에는 package.json에 명시된 패키지만 존재
- 의존성 패키지들은 .pnpm/ 내부에만 존재하며 직접적인 접근 불가
- 패키지들은 각자의 의존성 트리를 통해 의존성 패키지에 접근 가능
- 뭔말?
- a나 b 패키지가 c 패키지를 의존하고 있으면 node_modules에는 없지만 각자의 의존성 트리를 이용해 접근이 가능
- but 사용자는 직접 접근 불가능
- 디스크 공간 절약
- pnpm은 글로벌 저장소를 사용
- 하위에 프로젝트들은 최상위 node_modules에 있는 패키지를 참조하여 사용 가능
- 디스크에는 해당 패키지가 딱 1번만 저장
npm, yarn, pnpm 비교
- 설치 속도
# 실제 벤치마크 (대규모 프로젝트)
npm install: 45초
yarn install: 25초
pnpm install: 12초
# 캐시 있을 때
npm install: 20초
yarn install: 3초
pnpm install: 5초
- 디스크 사용량
npm:
프로젝트당 200MB × 10 = 2GB
Yarn Classic:
프로젝트당 200MB × 10 = 2GB
Yarn Berry (PnP):
프로젝트당 60MB × 10 = 600MB
pnpm:
글로벌 저장소 200MB + (링크 × 10) = 250MB
- 호환성
# npm
✅ 모든 Node.js 프로젝트
✅ 모든 패키지
✅ 레거시 빌드 도구
# Yarn Classic
✅ 거의 모든 프로젝트
✅ npm과 거의 동일한 호환성
# Yarn Berry (PnP)
⚠️ 일부 패키지 호환 문제
⚠️ native module 지원 제한
✅ nodeLinker: node-modules 옵션으로 우회 가능
# pnpm
✅ 대부분의 프로젝트
⚠️ 일부 잘못 작성된 패키지 (유령 의존성 활용)
✅ shamefully-hoist 옵션으로 우회 가능