컨텐츠
들어가기 전에
이런 분들이 읽으시면 좋을 것 같아요.
- 지금까지 패키지 매니저를 당연하게 써왔지만, 왜 쓰는지 설명할 수 없는 개발자
- 패키지 매니저의 동작 방식을 설명할 수 없는 개발자
- 현재 사용 중인 패키지 매니저와 다른 패키지 매니저 간의 차이점을 명확히 설명할 수 없는 개발자
패키지 매니저와의 만남
여러분이 프론트엔드 개발자로 프로젝트를 진행하게 된다면, 거의 무조건 패키지 매니저라는 녀석을 만나게 됩니다. (달랑 하나의 자바스크립트 파일을 만들어 아주 간단한 문법을 작성하는 것이 아니라면 말입니다.)
프로젝트를 깃에서 클론받은 뒤, npm install
, yarn install
과 같은 명령어를 실행했을 것이고, 프로젝트를 로컬 환경에서 실행할 때도 npm run dev
, npm run start
, yarn dev
등 비슷비슷한 명령어들을 수도 없이 실행했을 것입니다.
첫 프론트엔드 개발을 시작할 때는 프로젝트를 실행하는 것조차 버겁기 때문에 지금 내가 뭘 설치하고 있는 거고, 어떻게 이걸 실행했는지에는 관심이 없습니다. 단지 실행이 되었다는 것. 그것만이 중요하죠.
중간에 동료 개발자가 다른 패키지 매니저를 사용하자고 할 때도 그걸 왜 쓰는지에 대해 그렇게 궁금하지 않습니다. 사용법은 비슷비슷하거든요. (설치.. 실행.. 빌드..!)

하지만 자연스럽게 개발 경험이 풍부해지고, 시야가 넓어지고, 프로젝트를 새로 진행하고 초기 설정을 하는 경우가 많아지면서, 늘 당연하게 써왔던 이 패키지 매니저에 은근한 궁금증이 생깁니다.
- 근데 그래서 패키지 매니저 얘가 왜 필요하지?
- 얘가 없으면 어떻게 되는 거야?
- 종류는 또 왜 이렇게 많아?
- 뭐가 좋길래 이거 말고 이걸 쓰는 거야?
지금부터 저와 함께 위 질문들을 하나씩 풀어보도록 합시다.
패키지 매니저의 등장
모든 기술의 등장에는 그 이유가 있습니다.
리부트 서버 시작
저와 함께 잠시 한 가지 상상을 해봅시다.
이제 여러분은 외부에서 아무런 코드도 가져올 수 없습니다.
useState
훅이 필요하다면 직접 클로저를 통해 구현해서 사용해야 합니다. Axios
의 JSON 파싱, 예외처리나, React Query
의 데이터 캐싱이 필요하다면 직접 다 구현해야 합니다.
개발 리부트 서버 시작입니다. 개발 실력을 키우지 못하면 라이브러리의 편리함은 없습니다!
React Query
를 쓰고 싶지만 어떻게 구현해야 할지 막막했던 여러분은 뛰어난 사교성으로 개발을 아주 잘하는 친구 A에게 연락해 친구가 편리하게 쓰기 위해 직접 구현한 친구 표 라이브러리, Friend Query
를 드르륵 긁어와서 여러분의 프로젝트에 복사해서 사용하게 됩니다.

앗! 그런데 에러가 발생합니다. 여러분의 프로젝트와 친구에게서 복사해온 Friend Query
에서 같은 변수명을 사용하고 있었네요. 아주 사소하죠. 간단한 문제이므로 빠르게 수정하여 문제를 해결합니다.
그렇게 잠시 편안한 시간을 즐깁니다.
Friend Query
에 문제가 생기기 전까지..
Friend Query
에 알 수 없는 문제가 발생했고, 여러분의 프로젝트가 잠시 모두 멈춥니다. Friend Query
의 내부 로직을 모르는 여러분은 친구 A에게 연락해 픽스를 요청합니다.
똑똑한 A는 이를 곧바로 수정해 픽스된 코드를 보내주었지만, 이 코드를 실행한 여러분은 또 다른 에러를 맞닥뜨립니다. 친구는 아주 잘만 실행된다고 하는데 말이죠.
이런, 알고 보니 A가 Friend Query
를 수정할 때 또 다른 친구 B의 코드를 긁어와서 활용했다고 합니다.
이제 여러분은 어떻게든 또 다른 친구 B의 코드를 구해서 여러분의 프로젝트에 복사해와야 할 것입니다. A의 코드를 사용하기 위해서 말이죠. 부디 B의 코드가 친구 A에게 준 버전과 같길 바래야겠네요 !
어떤가요? 잠시 상상만 해보아도 버겁죠?
패키지 매니저가 존재하지 않는 혼돈의 세상에는 이런 일이 벌어집니다.
- 의존성 수동 설치 및 관리
- 필요한 라이브러리의 소스 코드를 직접 설치
- 프로젝트 폴더에 수동으로 복사/붙여넣기
- 버전 관리와 업데이트를 수작업으로 진행
- 표준 부재
- 라이브러리마다 다른 설치 방법
- 일관된 버전 관리 체계 부재
- 글로벌 네임스페이스 충돌 위험
- 의존성 지옥
- 라이브러리 간의 의존성 관계를 수동으로 파악
- 서로 다른 버전의 충돌 문제
- 중복 라이브러리 설치로 인한 프로젝트 크기 증가
패키지 매니저의 역할
패키지 매니저는 자바스크립트 프로젝트에서 사용하는 의존성 관리 도구입니다. 여기서 의존성이라고 함은, 말 그대로 프로젝트가 실행되기 위해 의존하는 외부 라이브러리, 모듈, 플러그인 등을 의미합니다.
즉 패키지 매니저는 이러한 의존성들을 손쉽게 설치하고, 관리하는 역할을 합니다. 패키지 매니저의 주요 역할은 다음과 같습니다.
- 자동화된 의존성 관리
- 의존성 버전 명시 및 자동 관리
- 단일 명령어로 모든 의존성 설치 자동화
- 업데이트 및 삭제 자동화
- 표준화된 패키지 저장소
- npm registry를 통한 표준화된 패키지 배포
- 검증된 패키지의 이름과 버전을 지정하여 쉽게 설치
- 모든 패키지가 같은 방식으로 설치 및 관리
- 의존성 최적화
- 의존성 트리 자동 분석 및 설치
- 의존성 간 버전 충돌 감지 및 해결
- 중복된 패키지 제거로 디스크 공간 절약
- 프로젝트 생명주기 관리
- 일관된 명령어 체계 (
install
,run
,build
등) - 스크립트를 통한 작업 자동화
- 프로젝트 빌드 및 배포 프로세스 통합
- 일관된 명령어 체계 (
이처럼 더 이상 외부 라이브러리 없이는 개발할 수 없는 현시대에서 패키지 매니저는 빠뜨릴 수 없는 도구입니다.
패키지 매니저 동작 방식
이제 패키지 매니저가 왜 현대 개발에서 필수적인지는 감이 딱 잡혔습니다.
그럼 동작 방식을 알아보면서 조금만 더 기술적으로 알아볼게요.
패키지 매니저는 기본적으로 Resolution
, Fetch
, Link
이렇게 3가지 단계로 동작합니다.
Resolution 단계
사용되는 의존성 버전 고정의존성의 다른 의존성 확인의존성의 다른 의존성 버전 고정
Resolution은 말 그대로 해결
단계입니다. 무언가 문제가 발생했기 때문에 해결 단계가 생겼겠죠?
우선 첫 번째 문제, 라이브러리 버전 고정입니다. 앞선 예시에서 여러분은 친구 A의 Friend Query
를 사용하다 에러가 터지자, 픽스된 버전을 받아 사용하기 시작했습니다. 즉 앞으로도 여러분의 프로젝트에선 에러가 해결된 버전 이후의 Friend Query를 사용해야 합니다.
패키지 매니저는 이를 수행하기 위해 package.json
파일에 의존성의 버전을 명시하고, 버전 표기의 규칙에 따라 의존성의 버전을 선택합니다. 예를 들어, "react": "^18.2.0"
라고 명시되어 있으면, ^
이 나타내는 규칙에 따라 ≥ 18.2.0
, < 19.0.0
사이의 버전을 사용합니다. 이때 패키지 매니저는 해당 범위를 만족하는 선에서 가능한 최신 버전을 사용하려고 합니다.
두 번째 발생하는 문제는 의존성이 사용하는 또 다른 의존성, 의존성의 의존성 문제입니다.
앞선 예시에서, 여러분은 버그가 픽스된 버전의 Friend Query
를 받아 사용했습니다. 하지만 픽스된 Friend Query
는 또 다른 친구 B의 코드(의존성)를 사용하고 있었죠. 그래서 여러분은 또 다른 친구 B의 코드를 구해와야만 Friend Query
를 사용할 수 있었습니다.
만약 여러분이 B의 코드를 받아왔지만, 픽스된 Friend Query
에서 사용했던 B의 코드와 버전이 다르다면, 같은 동작을 할 것이라 보장할 수 있을까요?
보장할 수 없습니다. 따라서 의존성이 또 어떤 의존성을 가지는지 확인하는 작업이 꼭 필요합니다. 패키지 매니저는 이런 의존성의 의존성 버전을 고정하여 그 결과물을 package-lock.json
혹은 yarn.lock
파일에 저장합니다.
그렇기 때문에 package-lock.json
혹은 yarn.lock
파일이 없으면 같은 package.json
을 사용하지만 설치되는 의존성 버전이 매우 달라질 수 있습니다.
이렇게 철저한 의존성 버전 고정을 통해 의존성 버전 문제를 해결하는 단계가 바로 Resolution 단계입니다.
Fetch 단계
결정된 버전의 의존성들을 다운로드
Fetch는 이름에서 알 수 있다시피, Resolution 단계에서 결정된 버전을 기반으로 의존성들을 다운로드하는 단계입니다.
Link 단계
Resolution/Fetch 된 의존성들을 소스 코드에서 사용할 수 있는 환경을 제공
Link 단계는 Fetch 단계에서 다운로드 받은 의존성들을 실제로 어떻게 프로젝트에 저장하고, 탐색하고, 사용할지를 결정하는 단계로, 패키지 매니저의 마침표이자 핵심이라고 볼 수 있습니다.
대표적인 패키지 매니저들의 주요 차이점 또한 이 Link 단계에서 발생하는데요. 한 번 차근차근 살펴보도록 합시다.
대표적인 패키지 매니저
현재 패키지 매니저는 대표적으로 아래 4가지가 존재합니다.
- NPM
- Yarn Classic
- Yarn Berry
- PNPM
NPM (Node Package Manager)
먼저 여러분이 아마 가장 익숙할 패키지 매니저, NPM입니다.
NPM의 Link 단계에서는, package.json
에서 명시하는 모든 의존성을 node_modules
폴더 밑에 하나하나씩 모두 저장합니다.
만약 프로젝트에서 사용하는 의존성이 또 다른 의존성을 사용한다면, 그 의존성도 또다시 해당 의존성의 node_modules
폴더 밑에 저장하게 됩니다.
아래 구조는 호이스팅 메커니즘이 적용되기 전 NPM의 중복 설치를 보여주는 예시입니다.
node_modules가 계속 반복되는 형태를 볼 수 있다.your-reboot-project ├── node_modules │ ├── friend-query │ │ └── node_modules │ │ └── other-library │ │ └── node_modules │ │ └── another-other-library │ └── other-library │ └── node_modules │ └── another-other-library ...
또한 위의 예시에서 your-reboot-project는 friend-query와 other-library 두 의존성을 직접 사용하고 있는 걸 확인할 수 있습니다. 이때, friend-query 의존성에서도 other-library를 사용하는데요. 이때 other-library와 그 의존성인 another-other-library까지 중복 설치되는 것을 볼 수 있습니다.
이런 방식은 프로젝트가 커질수록 크게 세 가지 문제가 발생합니다.
- 의존성들이 실제 파일 시스템에 중복으로 설치되어 디스크 공간을 많이 차지하게 됩니다.
- 예를 들어 만약 사용 중인 100가지 의존성이 모두 똑같은 react 의존성을 사용한다고 가정할 경우, 의존성의 개수만큼
node_modules/의존성1~100/node_modules/react/node_modules/...
이런 식으로 react가 100번 저장됩니다.
- 예를 들어 만약 사용 중인 100가지 의존성이 모두 똑같은 react 의존성을 사용한다고 가정할 경우, 의존성의 개수만큼

- 패키지에서 import를 통해 의존성을 읽으려고 할 경우, 해당 의존성을 찾기 위해 현재 패키지의 node_modules 폴더부터 의존성을 찾고, 만약 찾지 못했을 때 상위 node_modules로 타고 올라가면서 수많은
node_modules
****를 탐색하게 되는데, 이러한 I/O 호출이 반복되어 성능 저하가 발생할 수 있습니다.- Node.js의 모듈 해석 알고리즘(Module Resolution Algorithm)
- 상위 node_modules를 탐색하는 특성 때문에, 패키지의 상위 디렉터리 환경에 따라 의존성 탐색 여부가 달라질 수 있습니다.
개발에 있어서 2번, 3번 문제처럼 환경에 따라 동작이 변하는 것은 상당히 위험한 문제입니다. 평소에 잘 동작하다가도 언제 어느 날 갑자기 문제가 터질지 알 수 없고, 문제가 터지더라도 이를 재현하기가 매우 까다롭기 때문입니다.
호이스팅과 유령 의존성

위 그림의 왼쪽을 살펴보면 package-1은 A, C, D 의존성을 직접 가지고 있다는 것을 확인할 수 있습니다. 이때 A는 B를 의존성으로 가지고 있고, 또 C는 그런 A를 의존성으로 가집니다.
따라서 만약 호이스팅을 하기 전이라면 A -> B 의 의존성은 package-1 바로 아래의 node_modules
에 한 번, 그리고 C 아래 node_modules
에 한 번, 총 두 번 설치되어 디스크 공간을 낭비하게 됩니다.
이를 해결하기 위해 NPM과 Yarn Classic에서는 호이스팅(Hoisting) 이라는 방법을 사용합니다.
호이스팅이란, 프로젝트의 모든 의존성을 검토한 뒤, 여러 패키지에서 공유할 수 있는 버전의 의존성이 존재한다면, 이들을 모두 최상단으로 끌어올리는 것을 말합니다.
하지만 이렇게 호이스팅을 하게 되면, 내 프로젝트에서 현재 직접 의존하고 있지 않은 라이브러리를 require할 수 있게 됩니다. 왼쪽 트리에서는 기본적으로 A, C, D를 의존하고 있기 때문에 B를 의존성으로 불러올 수 없었습니다. 자신이 루트이고, 현재 node_modules
폴더에는 A, C, D 밖에 없으니까 말이죠. 하지만 우측 트리로 호이스팅 되면서 이제 B 의존성을 직접 불러올 수 있게 되었습니다.
이러한 현상을 유령 의존성(Phantom Dependency) 이라고 부릅니다.
유령 의존성 현상이 발생할 경우, 여러분이 전혀 모르는, package.json
에 명시되어있지 않은 라이브러리를 사용할 수 있게 됩니다. 그리고 이런 현상은 다른 의존성을 삭제할 때 다시 소리소문없이 사라지기도 합니다.
여기서 있는지도 모르는 라이브러리를 어떻게 사용하냐! 뭐가 문제냐! 싶을 수도 있습니다.
만약 여러분의 프로젝트가 moment
라는 날짜 라이브러리를 사용하고 있다고 가정합시다. 이 moment
라이브러리는 정말 흔히 사용하는 lodash
라이브러리를 사용합니다.
이럴 경우 호이스팅으로 인해 lodash
는 프로젝트의 package.json
에 명시되어 있지 않지만, 직접 사용할 수 있게 됩니다. 그렇게 평소에 항상 lodash
를 사용했던 여러분은 당연하게 lodash
를 사용하는 코드를 작성하고 개발을 진행합니다.
그러던 어느 날 팀에서 날짜 라이브러리를 moment
대신 dayjs
를 사용하기로 합니다.
그렇게 moment
를 제거하는 순간, 아무런 관련이 없던 lodash
관련 코드들이 전부 터져버리는 걸 보게 될 겁니다. 잘 사용하고 있었던 lodash
가 사실 moment
의 하위 의존성 호이스팅으로 인한 유령 의존성이었기 때문입니다.
Yarn Classic (v1)
Yarn Classic은 2016년 Facebook에서 NPM의 보안 및 성능 문제를 개선하여 NPM을 대체하기 위해 새롭게 개발된 패키지 매니저입니다.
출시 당시 NPM과 비교했을 때는 아래와 같은 강점이 있었습니다.
- 병렬 설치로 인한 빠른 속도
- NPM은 패키지를 차례대로 설치하는 데 비해, Yarn은 여러 패키지를 병렬로 처리
- 캐싱
- 다운로드 받은 패키지를 캐싱하여 재사용
- 보안성
- 체크섬(checksum) 검증을 통한 패키지 무결성 검사
- 패키지 자동 실행 제한
하지만 현재는 NPM도 아래와 같은 많은 발전을 이루어 위 장점들이 대부분 상쇄되었습니다.
- NPM도 병렬 설치 지원
- NPM도 패키지 캐싱 시스템 도입
- NPM도 보안 기능 강화 (체크섬 검증 등)
따라서 현재 이러한 차이점들은 매우 미묘하며, 버전 잠금 파일명(yarn.lock
, package-lock.json
)이나, 스크립트 명령어(npm
대신 yarn
명령어 사용)의 차이를 빼면 이제 거의 같다고 봐도 무방하다고 볼 수 있습니다.
PNPM (Performant NPM)
PNPM은 기존 NPM의 node_modules
폴더를 그대로 유지하면서 성능과 용량을 개선한 패키지 매니저입니다.
따라서 NPM을 사용하던 프로젝트도 PNPM으로 전환하면 아주 쉽게 성능과 용량이 개선될 수 있습니다.
PNPM이 이를 가능하게 한 것은 바로 글로벌 저장소(.pnpm-store
)와 하드 링크/심볼릭 링크를 통한 접근 방식 때문인데요. 조금 자세하게 알아보도록 하겠습니다.
**Linux의 하드 링크/심볼링 링크 개념을 쉽게 설명드려 볼게요!**
우리는 보통 파일 안에 데이터가 있다고 생각합니다. 따라서, 어떤 데이터를 찾고자 할 때 클릭 => 파일 A -> 데이터
의 흐름으로 데이터를 찾는다고 생각합니다.
하지만 그 사이에는 inode
라는 녀석이 하나 더 있습니다. inode
는 파일 시스템에서 파일의 실제 데이터가 어디에 저장되어 있는지 가리키는 포인터입니다.
그리고 우리가 흔히 부르는 파일
은 사실 이 inode
****를 가리키는 포인터입니다.
즉, 데이터를 열기까지 클릭 => 파일 A -> inode #123 -> 실제 데이터
이런 흐름으로 동작하는 것이죠.
하드 링크
하드 링크는 원본 파일이 가리키는 inode를 동일하게 가리킵니다.
파일 A → inode #123 → 실제 데이터 파일 B ↗
따라서 파일 A(원본 파일)가 삭제되어도, 파일 B(하드 링크)는 여전히 원본 파일의 inode를 가리키고 있기 때문에, 파일 B를 통해 원본 데이터를 찾을 수 있습니다.
심볼링 링크 (소프트 링크)
심볼릭 링크는 파일이나 폴더를 가리키는 포인터 역할을 합니다. 쉽게 Window OS의 바로가기라고 생각하시면 편합니다.
따라서 원본 파일을 삭제하거나, 이동할 경우 심볼릭 링크는 더 이상 존재하지 않는 파일/디렉터리를 가리키게 되어 동작하지 않게 됩니다.
파일 A → inode #123 → 실제 데이터 파일 B → inode #456 → "파일 A의 경로"
PNPM은 실제 의존성 데이터를 글로벌 저장소에 딱 한 번만 저장합니다. 이 글로벌 저장소는 운영체제의 특정 위치에 저장됩니다:
- Windows:
%LOCALAPPDATA%/pnpm/store
- macOS:
~/Library/pnpm/store
- Linux:
~/.local/share/pnpm/store
그리고 프로젝트의 node_modules/.pnpm
디렉터리에는 이 글로벌 저장소의 패키지들을 참조하는 하드 링크들이 저장됩니다.

예를 들어 여러분이 프로젝트에서 import React from 'react'
를 사용한다고 가정해봅시다.
우선 처음에는 node_modules/react
를 찾아갑니다. 이후 node_modules/react
는 심볼릭 링크(바로가기)를 통해 node_modules/.pnpm/react@18.2.0/node_modules/react
의 파일 경로를 가리킵니다. 그리고 node_modules/.pnpm/react@18.2.0/node_modules/react
는 하드 링크를 통해 글로벌 저장소의 실제 react
패키지를 가리킵니다.
react 패키지 탐색 과정node_modules/ ├── .pnpm/ │ └── react@18.2.0/ │ └── node_modules/ │ └── react/ <--- (.pnpm-store/react 와 하드링크) └── react/ <--- (node_modules/.pnpm/react@18.2.0/node_modules/react 로 가는 심볼릭 링크)
이렇게 node_modules
폴더는 더이상 실제 의존성 데이터를 쌓아두는 것이 아니라 심볼릭 링크와 하드 링크를 통해 실제 데이터를 참조하는 역할만 수행하기 때문에 디렉터리 크기가 무척 작아집니다.
결국 단 한 번만 의존성을 저장하기 때문에, 의존성이 필요한 모든 패키지에서 불필요한 쓰기 작업을 반복할 필요가 없어져 디스크 공간 효율성과 속도 모두를 잡을 수 있었던 것이죠.
또한 PNPM은 node_modules
폴더를 유지하기 때문에 NPM과의 호환성이 매우 뛰어납니다.
정리하자면, PNPM은 다음과 같은 장점을 가집니다.
장점
- 디스크 공간 효율성
- 실제 의존성 데이터를 글로벌 저장소에 딱 한 번만 저장
- 하드 링크를 통한 중복 설치 방지
- 빠른 설치 속도
- 이미 설치된 패키지는 하드 링크로 즉시 연결하고, 새로운 패키지만 다운로드하여 설치 시간 단축
- 병렬 설치 지원
- 엄격한 의존성 관리
package.json
에 명시된 의존성만 사용 가능- 유령 의존성 문제 해결
- NPM 호환성
node_modules
구조를 유지하여 기존 프로젝트와 호환- NPM 명령어와 유사한 사용법으로 쉬운 전환
- NPM 레지스트리 완벽 지원
하지만 PNPM의 NPM 호환성은 한 가지 단점이 있는데, 의존성의 의존성 등에 node_modules
폴더가 중첩적으로 존재하는 구조 자체는 바뀌지 않기 때문에 전체 의존성 트리를 파악하고, node_modules
를 타고타고 돌면서 의존성에 링크를 하나씩 걸고 탐색하는 과정에서 버벅이는 이슈가 발생할 수 있습니다.
Yarn Berry (v2)
그런데, 굳이 node_modules
를 계속 사용하는 이유가 뭘까요? 언제까지고 의존성을 탐색하기 위해 순회해야 하는 걸까요?
Yarn Berry는 이러한 node_modules
파일 시스템 구조로부터 벗어나기 위해 PnP(Plug'n'Play) 전략을 사용합니다.
Yarn Berry는 node_modules
를 생성하지 않습니다. 대신 .yarn/cache
폴더 밑에 zip
아카이브 파일 형태로 실제 의존성 데이터를 압축된 형태로 저장하고, .pnp.cjs
파일에 의존성을 찾을 수 있는 정보를 기록합니다.
.yarn/cache
폴더 밑에는 각 의존성의 버전마다 단 하나의 zip 아카이브만이 존재합니다.
.pnp.cjs
를 이용하면 디스크 I/O 없이 어떤 의존성이 어떤 의존성, 버전에 의존하는지, 또 각 의존성은 어디에 위치하는지 등의 정보를 바로 알 수 있습니다.
간단하게 .pnp.cjs
파일을 살펴보면 아래와 같은 형태로 이루어져 있습니다.
{ "packageRegistryData": [ /* react 패키지 중에서 */ ["react", [ /* npm:17.0.1 버전은 */ ["npm:17.0.1", { /* 이 위치에 있고 */ "packageLocation": "./.yarn/cache/react-npm-17.0.1-95143125fc-a25f73ef98.zip/node_modules/react/", /* 이 의존성들에 의존한다. */ "packageDependencies": [ ["loose-envify", "npm:1.4.0"], ["object-assign", "npm:4.1.1"] ], /* 패키지가 .yarn/cache 내 zip 파일에 물리적으로 존재한다. */ "linkType": "HARD" }], ]], ] }
이처럼 의존하는 의존성 패키지의 위치와 의존성의 의존성 목록까지 모두 명시된 것을 확인할 수 있습니다.
이를 활용하여 import 문으로 의존성을 불러올 때 디스크 I/O 없이 바로 의존성을 위치를 찾아 사용할 수 있게 되는 거죠.
Zero-install
Yarn Berry는 의존성 버전마다 하나의 Zip 아카이브를 가지고, node_modules
폴더 구조마저 사라져버려 의존성을 구성하는 파일의 숫자와 용량이 크게 줄어들었습니다.
이렇게 의존성 패키지들이 감당 가능할 정도로 사이즈가 줄어들면, 굳이 의존성을 매번 일일이 설치해야 할까요? 굳이 Resolution/Fetch 단계를 거쳐야 할까요? 그냥 Fetch 단계를 완료한 상태를 공유하면 늘 동일한 상태가 유지되지 않을까요?
이러한 생각을 바탕으로 Yarn Berry는 의존성을 버전 관리에 포함하는 Zero-install 개념을 도입했습니다.
의존성을 버전 관리에 포함하게 되면 새로 레포지토리를 클론하거나 브랜치를 바꾸었을 때 매번 의존성을 따로 설치하지 않아도 됩니다.
결과적으로 설치된 의존성 패키지들이 늘 같은 상태를 유지하기 때문에 환경 세팅과 잘못된 의존성 버전으로 말미암은 문제가 완전히 해결되는 거죠.
이러한 Zero-install은 Yarn Berry에서만 활용하는 기술은 아닙니다.
Resolution 단계와 Fetch 단계가 완료된 결과물을 버전 관리 시스템에 포함하는 개념을 모두 Zero-install이라고 부릅니다.
따라서 NPM을 사용하면서도 Zero-install을 사용할 수 있습니다. 하지만 NPM 방식을 사용할 경우 중복된 의존성과 복잡한 node_modules
트리 구조로 용량이 커지기 때문에 경제적이지 않아 사용하지 않는 것이지요.
인터페이스 모듈화 및 다양한 플러그인
이외에도 Yarn Berry는 패키지 매니저의 각 단계 (Resolution, Fetch, Install)의 인터페이스가 다른 패키지 매니저와 비교하면 잘 분리되어 있고 모듈화되어 있다는 특징이 있습니다.
그리고 다양한 플러그인을 제공하여 확장 가능성이 높다는 장점도 있습니다.
따라서 매우 방대한 양의 코드와 거대한 규모의 모노레포를 사용하는 토스와 같은 여러 IT 기업에서 구조적인 확장과 유지보수, 용이한 CI를 위해 주로 사용하고 있습니다.
결론적으로, Yarn Berry는 다음과 같은 장점이 있습니다.
장점
- 설치 및 실행 속도 개선
node_modules
트리 구조와 폴더를 생성하지 않아 설치가 매우 빠름.pnp.cjs
파일을 통해 디스크 I/O 없이 의존성을 즉시 찾아 사용- 의존성 탐색을 위한 파일 시스템 순회가 없어 실행 속도가 빠름
- 디스크 공간 효율성
- 각 의존성 패키지의 버전마다 하나의 Zip 아카이브만 보관
- 압축된 형태로 저장되어 디스크 공간을 크게 절약
- Zero-install 적용 시에도 용량 부담이 적음
- 엄격한 의존성 관리
- 각 패키지는 자신의 package.json에 명시된 의존성에만 접근 가능
- 유령 의존성 현상 원천 차단
- 의존성 관계가 명확하여 디버깅이 용이
- Zero-install 지원
- 의존성을 Git 저장소에 포함하여 별도 설치 과정 불필요
- CI/CD 환경에서 의존성 설치 시간 제로
- 팀원 간 같은 의존성 버전 보장
- 모듈화된 아키텍처
- Resolution, Fetch, Link 단계가 명확히 분리된 설계
- 다양한 플러그인을 통한 확장 가능
- 커스터마이징이 용이한 구조
마무리하며
지금까지 패키지 매니저가 왜 필요한지, 어떻게 동작하는지, 그리고 대표적인 패키지 매니저들의 특징들까지 함께 알아보았습니다.
이제 여러분은 프로젝트에서 어떤 패키지 매니저를 사용할지 기술적으로, 또 능동적으로 비교하여 선택할 수 있을 겁니다.

그럼에도 불구하고 패키지 매니저를 선택하기 어려워하시는 분들께 제 개인적인 패키지 매니저 선택 기준을 공유드리고 이 포스팅을 마치려고 합니다.
패키지 매니저 선택 기준
만약 기존에 진행하던 프로젝트에 참가한 경우에는 되도록 기존 패키지 매니저를 사용하는 것을 권장합니다.
하지만 만약 의존성 관리 문제가 발생하거나, 프로젝트 규모가 커져 성능 이슈가 발생할 때 패키지 매니저를 변경하는 것을 권장합니다.
개인적으로 Yarn Classic은 이제 굳이 사용할 필요성을 느끼지 않아 다음의 3가지 패키지 매니저 중에 선택합니다.
- NPM
- 가장 기본적이고 안정적인 선택
- 별도의 학습 비용이 필요 없음
- 최근 많은 성능 개선이 이루어짐
- 하지만 여전히
node_modules
중첩 구조로 인한 성능/용량 이슈 존재
- PNPM
- 디스크 공간 효율성과 설치 속도가 중요할 때
- 유령 의존성 문제를 해결하고 싶을 때
- NPM 생태계와의 호환성을 유지하고 싶을 때
node_modules
구조를 유지하면서 성능 개선이 필요할 때
- Yarn Berry
node_modules
구조 자체를 제거하고 싶을 때- Zero-install을 통해 의존성 설치 과정을 완전히 제거하고 싶을 때
- 의존성 관리의 혁신적인 변화를 원할 때
- 하지만 일부 라이브러리와의 호환성 문제 가능성 존재
프로젝트 규모에 따른 패키지 매니저 선택 기준
- 소규모 프로젝트 / 학습용 프로젝트
- 👍 NPM
- 진입 장벽이 낮고 문제 해결을 위한 레퍼런스가 풍부
- 중규모 프로젝트
- 👍 PNPM
- NPM의 장점을 유지하면서 성능과 디스크 공간 효율성 개선
- 기존 프로젝트에서 마이그레이션하기도 용이
- 대규모 프로젝트 / 모노레포 프로젝트
- 👍 Yarn Berry
- Zero-install과 PnP를 통한 최적화된 의존성 관리
- 특히 모노레포 환경에서 workspace 기능 활용도가 높음
- 레거시 라이브러리가 많은 프로젝트
- 👍 PNPM
node_modules
구조를 유지하여 호환성 문제 최소화- ⚠️ Yarn Berry는 PnP로 인한 호환성 이슈 가능성 존재
- CI/CD 최적화가 필요한 프로젝트
- 👍 Yarn Berry
- Zero-install을 통한 CI 시간 대폭 감소
- 👍 PNPM
- 글로벌 저장소를 통한 캐싱으로 설치 시간 감소
출처
Toss Tech - 패키지 매니저의 과거, 토스의 선택, 그리고 미래