npm, Yarn, pnpm.
masonfe-hyunsu
🧵 1. 개요.

프론트 프로젝트를 열면 거의 항상
package.json이 있고, 옆에package-lock.json이거나yarn.lock,pnpm-lock.yaml이 붙어 있다.팀마다 쓰는 도구가 다르고, CI에서 설치가 어긋나면 로컬에선 됐는데 배포에서만 깨지는 일도 생긴다.
npm, Yarn, pnpm은 서로 어떤 차이가 있고, 어떤 기준으로 선택해야 할지 정리해 본다.
🧭 2. 대표적인 차이점.
셋 다 하는 큰 그림은 같다.
package.json에 적힌 패키지를 받아 오고, 버전을 lockfile에 고정해서 다음에 설치해도 같게 맞춘다.차이는 받은 파일을 프로젝트에 어떻게 풀어 두는지에 따라 달라진다.
node_modules에 몽땅 펼쳐 둘지(npm), 링크만 걸어 둘지(pnpm), 아니면 폴더 없이 경로표만 둘지(Yarn PnP).
왜 구조가 중요한가.
파일을 넓게 풀어 두면 대부분 도구가 잘 돌아간다. 대신
package.json에 안 적은 패키지도 import가 되는 일(phantom dependency)이 생기기 쉽다. 적지도 않은데 우연히 보이는 dependency라서다. 지금은 되는데, 나중에 옆 패키지 버전이 바뀌면 갑자기 깨질 수 있다.링크나 경로표처럼 엄격하게 두면, 안 적은 패키지는 아예 안 보인다. 그 실수는 막아주지만, 예전에 느슨한 구조를 전제로 만든 패키지와는 가끔 부딪친다.

왼쪽부터 npm(펼치기), pnpm(링크), Yarn(경로표)이다. 예전 Yarn v1은 npm처럼 펼치는 쪽에 가깝다.
📦 3. npm : 기본값, 최대 호환.
Node와 같이 오는 기본 패키지 매니저다. 별도 설치 없이 쓰는 팀이 많다.
기본은 hoisting(공통 의존성을 위쪽으로 끌어올리는 방식)으로
node_modules를 평평하게 만든다.lockfile은
package-lock.json. CI에서는 보통npm ci로 lock을 기준으로 깨끗이 깐다.
언제 사용하는 게 적합할까.
이미 npm이 기본인 CI, Docker 이미지처럼, 새 도구 설치 없이 가고 싶을 때.
"일단 돌아가게"가 우선이고, 의존성 엄격함은 나중에 다듬어도 될 때.
아쉬운 점.
디스크에 같은 버전이 프로젝트마다 중복되기 쉽다.
평평한 트리 덕분에 선언하지 않은 패키지를 import 해도 로컬에선 통과하는 경우가 있다. 나중에 상위 패키지 버전이 바뀌면 갑자기 깨진다.
🧶 4. Yarn : Classic과 Modern.
Yarn을 말할 때 헷갈리기 쉬운 지점이 여기다. Yarn Classic(v1)과 Yarn Modern(Berry, v2+)은 체감이 꽤 다르다.
Classic은 예전에 빠른 설치와
yarn.lock으로 유명했다. 지금도 Classic만 쓰는 레포가 남아 있다.지금 이야기하는 Yarn(Modern)의 기본은 PnP(Plug'n'Play).
node_modules대신.pnp.cjs로 "패키지가 어디 있는지" 경로표를 만든다. (Yarn PnP)lockfile은
yarn.lock. CI에서는 보통yarn install --immutable로 lock을 건드리지 않고 깐다.
언제 사용하는 게 적합할까.
새 프로젝트에서 PnP를 시험해 볼 여유가 있을 때.
Classic으로 굳은 레포는 당장 PnP까지 가지 않고, Modern +
nodeLinker: node-modules로만 올려도 된다.PnP가 부담이면
.yarnrc.yml에서nodeLinker: node-modules로 되돌릴 수 있다.
아쉬운 점.
IDE, 일부 네이티브 툴, React Native/Expo처럼
node_modules를 전제로 하는 스택은 설정이 더 필요하다.Classic과 Modern을 같은 Yarn으로 묶으면 팀 안에서 말이 엇갈리기 쉽다.
🔗 5. pnpm : 스토어와 엄격함.
pnpm의 핵심은 content-addressable store(같은 패키지 파일을 디스크에 한 번만 두고 재사용하는 저장소)다.
프로젝트
node_modules에는 스토어에서 hard link로 가져오고, 보이는 쪽은 symlink로 묶는다. 루트에는 직접 선언한 의존성 위주로 보인다. (pnpm layout)lockfile은
pnpm-lock.yaml. CI에서는pnpm install --frozen-lockfile이npm ci에 가깝다.

여러 프로젝트가 같은 머신에서 같은 버전을 쓰면, 스토어 재사용 효과가 바로 느껴진다. 로컬보다 CI 캐시에서 더 티가 난다.
언제 사용하는 게 적합할까.
디스크, 반복 설치, 모노레포(workspaces)에서 이득을 보고 싶을 때.
phantom dependency를 줄이고, 적지도 않은 패키지가 우연히 import되는 일을 막고 싶을 때.
아쉬운 점.
symlink / hard link를 전제로 해서, 일부 도구, 배포 환경에서 추가 설정이 필요할 수 있다.
평평한 hoisting에 기대던 패키지는
public-hoist-pattern같은 조정이 필요할 때가 있다. 이건 버그라기보다 엄격함이 드러나는 과정에 가깝다.
⚖️ 6. 그래서 뭘 고르나.
하나를 정답으로 두기보다, 팀이 감당할 비용과 이득을 기준으로 고른다.
우선순위 | 기울이는 쪽 |
|---|---|
호환, 온보딩 최소 | npm |
엄격함 + 디스크/CI | pnpm |
PnP, 제로에 가까운 설치 실험 | Yarn Modern |
레거시 Classic 유지 | Yarn v1 유지 또는 Modern + |
같은 레포에 lockfile을 두 종류 두지 않는다. 한 도구로 고정하고, CI도 그 도구의 freeze 명령으로 맞춘다.
packageManager필드로 쓸 도구와 버전을 적어 두고, Corepack(프로젝트에 적힌 패키지 매니저 버전을 맞춰 주는 도구)로 맞추면 로컬 버전 차이를 줄일 수 있다. Node 25부터는 Corepack이 배포에 안 실려서, 쓸 거면 별도 설치가 필요하다.Bun도 패키지 매니저 역할을 하지만, 이 포스팅 범위는 npm / Yarn / pnpm의 의존성 레이아웃 차이에 둔다.
속도 벤치만 보고 바꾸면 후회하기 쉽다. 깨지는 방식이 바뀌는 쪽을 먼저 본다.
✅ 7. 정리.
셋의 공통 목표는 같다. 의존성 트리를 재현 가능하게 설치하는 것.
차이는 레이아웃이다. hoisting, symlink store, PnP.
지금은 개인 프로젝트와 모노레포에서 pnpm을 기본으로 두는 편이 편했다. 호환이 최우선인 레포는 npm을 유지한다.
Yarn은 Classic과 Modern을 같은 말로 묶지 않는 게 먼저다. PnP를 쓸지,
node-modules로 둘지부터 정한다.
댓글