2025.12.22·6

패키지 매니저부터 모노레포까지 (03) - npm, Yarn, pnpm은 왜 서로 다른 길로 갔을까

빠르다는 이유만으로 Yarn과 pnpm을 골랐던 시기를 지나, 이제는 설치 방식과 의존성 모델의 차이로 패키지 매니저를 이해하게 되었습니다.

패키지 매니저부터 모노레포까지 (03) - npm, Yarn, pnpm은 왜 서로 다른 길로 갔을까 대표 이미지
용어 보기접기/펼치기
Glossary

패키지 매니저를 이름으로만 고르면 "빠르다더라"에 흔들리기 쉽습니다. 설치 방식과 의존성 모델을 이해하고 나서야 왜 도구들이 다른 방향으로 발전했는지 보이기 시작했습니다.

들어가며

앞선 두 편에서 npm이 무엇인지, 그리고 왜 npm 하나로는 모든 문제가 끝나지 않았는지를 먼저 정리했습니다. 이제는 그다음 질문으로 넘어갈 차례입니다.

처음에는 패키지 매니저를 비교해서 고른 적이 거의 없었습니다. Yarn이 빠르다고 하면 Yarn을 썼고, Yarn Berry가 더 좋다고 하면 또 그쪽으로 넘어갔습니다. 팀이 쓰는 도구를 그대로 따라간 경우도 많았습니다.

당시에는 그 선택이 크게 이상하다고 느끼지 않았습니다. 어차피 install만 잘 되면 되는 것처럼 보였기 때문입니다. 그런데 《npm Deep Dive》를 읽고 나서 생각이 조금 달라졌습니다. 패키지 매니저의 차이는 명령어 취향이 아니라, 의존성을 어떻게 설치하고 해석할지에 대한 모델 차이에 더 가깝다는 점이 보였기 때문입니다.

이 글에서는 "누가 더 빠른가"를 가르는 벤치마크보다, npm, Yarn Classic, Yarn Berry, pnpm이 각각 어떤 문제를 풀려고 했는지와 왜 서로 다른 방향으로 발전했는지를 중심에 두고 보려고 합니다.

오프화이트 배경 위에 npm, Yarn, pnpm 로고가 같은 축으로 정렬된 비교 이미지

이 글은 npm, Yarn, pnpm을 같은 패키지 매니저 축에서 비교합니다.

npm은 여전히 기본 축입니다

가끔은 npm을 다른 도구와 같은 급의 "하나의 클라이언트" 정도로만 생각하게 됩니다. 하지만 npm은 지금도 Node 생태계의 기본 축에 가깝습니다. 공식 문서도 npm을 "the package manager for the Node JavaScript platform"이라고 설명하고 있고, 실제로는 CLI뿐 아니라 public registry, lockfile, workspace, audit, publish 흐름까지 함께 묶여 있습니다.

예전의 저는 npm을 "느리고 불편한 기본값" 정도로 단순하게 생각했던 적이 있습니다. 하지만 지금의 npm은 그 시절의 인상만으로 설명하기 어렵습니다. 공식 문서 기준으로도 package-lock.json, npm ci, workspaces, install-strategy, min-release-age 같은 운영 기능이 계속 강화되고 있습니다.

즉 npm은 "기본이라서 별 의미 없는 선택지"가 아니라, 가장 넓은 호환성과 표준 흐름을 담당하는 기본 축이라고 보는 편이 맞습니다.

Yarn Classic은 초기 npm의 불만을 빠르게 흡수했습니다

Yarn도 처음부터 지금 우리가 떠올리는 형태로 한 번에 나온 것은 아닙니다. 먼저 Classic이 있었고, 이후 Berry로 넘어가면서 문제를 보는 관점과 해법도 더 크게 달라졌습니다.

Yarn이 주목받기 시작한 이유는 꽤 분명했습니다. 초창기 npm을 쓰던 시기에는 설치 속도, lockfile 안정성, 오프라인 대응 같은 부분에서 불만이 많았습니다. Yarn Classic은 이 지점을 강하게 파고들었습니다.

Classic 문서만 봐도 흐름이 보입니다. Getting Started, Workspaces, Offline Mirror, Plug'n'Play 같은 항목이 비교적 전면에 있습니다. 특히 "npm에서 넘어오기 쉬운 경험"을 강조했고, 실제로 많은 팀이 lockfile과 속도 개선을 체감하면서 Yarn으로 이동했습니다.

중요한 점은 Yarn Classic이 단순히 "npm의 다른 문법"이 아니었다는 점입니다. 그 시기의 Yarn은 npm 사용자가 불편하게 느끼던 실무 문제를 더 빠르게 흡수한 도구에 가까웠습니다.

Yarn Berry는 설치 모델 자체를 바꾸려 했습니다

Yarn Berry부터는 이야기가 조금 달라집니다. 이 버전은 Classic의 연장선이라기보다, 패키지 설치를 바라보는 관점을 크게 바꾼 쪽에 가깝습니다.

대표적인 것이 Plug'n'Play, 즉 PnP입니다. 이름만 보면 어렵지만, 아주 단순하게 말하면 node_modules 폴더를 크게 펼쳐 놓는 대신 다른 방식으로 패키지를 찾아 쓰게 만드는 설치 모델이라고 이해하면 됩니다. 공식 문서에서도 Yarn은 PnP를 기본 설치 전략으로 설명합니다. 이 모델은 속도와 디스크 효율에서 강점이 있고, 무엇보다 유령 의존성을 더 강하게 막아냅니다.

다만 여기서 끝나지는 않습니다. Yarn은 PnP만 강요하지 않고 nodeLinker를 통해 pnp, pnpm, node-modules 세 가지 설치 모드를 모두 지원합니다. nodeLinker는 Yarn이 패키지를 어떤 방식으로 연결하고 배치할지 고르는 설정이라고 보면 됩니다. 즉 Modern Yarn의 핵심은 "새 철학을 기본값으로 두되, 현실적인 호환성 선택지도 함께 둔다"는 데 있다고 느꼈습니다.

최근의 Yarn 문서를 보면 이 방향은 더 분명합니다.

  • PnP는 여전히 기본 설치 전략입니다.
  • node-modulespnpm linker도 1급 시민으로 취급합니다.
  • npmMinimalAgeGate, hardened mode 같은 보안 기능도 강조합니다.

그래서 Yarn Berry는 "더 빠른 Yarn"이라기보다, 설치 모델과 보안 모델까지 다시 설계한 패키지 매니저라고 보는 편이 더 정확합니다.

pnpm은 node_modules를 유지하면서 더 엄격해지려 했습니다

pnpm이 흥미로운 이유는 극단적으로 Node의 기존 관성을 버리지는 않으면서도, 구조는 더 엄격하게 가져가려 했다는 점입니다.

공식 Motivation 문서를 보면 이 도구가 풀고 싶은 문제가 아주 직접적으로 나옵니다.

  • 여러 프로젝트가 같은 패키지를 쓸 때 디스크를 과하게 낭비하지 않는 것
  • 설치를 더 빠르게 만드는 것
  • 루트에 너무 많은 패키지가 hoist되면서 생기는 유령 의존성 문제를 줄이는 것

pnpm은 이를 위해 content-addressable store를 두고, 패키지 파일을 한 곳에 저장한 뒤 hard link와 symlink로 node_modules를 구성합니다. 겉으로는 node_modules를 유지하지만, 내부 구조는 npm이나 Yarn Classic과 꽤 다릅니다.

제가 pnpm을 선호하게 된 이유도 여기에 있습니다. 기존 Node 도구와의 호환성은 비교적 잘 가져가면서도, "왜 이 import가 되는가"를 좀 더 엄격하게 보게 만들어주기 때문입니다.

이 지점은 이미지보다 표로 보는 편이 더 정확합니다.

도구중심 역할설치 모델의 성격감수해야 할 점
npm생태계 기본 축전통적인 node_modules 흐름을 바탕으로 호환성과 표준 흐름을 넓힌다가장 무난하지만, 엄격성 자체를 전면에 내세우는 도구는 아니다
Yarn Berry설치 모델 재설계PnP를 기본값으로 두고 해석 방식을 적극적으로 바꾼다철학이 분명한 만큼 적응 비용과 호환성 점검이 필요하다
pnpm호환성을 남긴 엄격성node_modules를 유지하되 store와 link 구조로 경계를 더 엄격하게 만든다기존 관성과의 타협은 가능하지만 구조 차이를 이해해야 한다

핵심은 "누가 더 빠른가"보다 각 도구가 어디에 무게를 두는지입니다. npm은 표준축을 넓히고, Yarn Berry는 설치 모델을 다시 설계했으며, pnpm은 호환성과 엄격성 사이의 절충점을 만들었습니다.

유령 의존성은 왜 실제 문제인가

설치 모델 차이가 실제 문제로 드러나는 대표 사례가 바로 유령 의존성입니다.

이 주제에서 가장 인상 깊었던 개념 중 하나가 유령 의존성입니다. 패키지가 자신의 package.json에는 선언하지 않았지만, 우연히 hoisting된 덕분에 import가 되어버리는 상황을 말합니다.

겉으로는 아무 문제가 없어 보입니다. 로컬에서도 돌아가고, 팀원 환경에서도 얼추 돌아갈 수 있습니다. 문제는 이 코드가 패키지로 배포되거나, 설치 구조가 조금만 달라져도 쉽게 깨질 수 있다는 점입니다.

예를 들면 이런 경우입니다.

// package.json에는 lodash가 없지만
// 루트에 hoist되어 있어서 우연히 동작하는 경우
import { debounce } from "lodash";

이 코드는 당장은 동작할 수 있습니다. 하지만 자신의 의존성을 스스로 명시하지 않는 코드이기 때문에, 구조가 바뀌는 순간 매우 취약해집니다.

Yarn PnP는 이런 문제를 강하게 드러내는 편이고, pnpm도 기본 구조상 루트에서 아무 패키지나 쉽게 참조하는 흐름을 덜 허용합니다. 반면 전통적인 hoisted node_modules에서는 이런 문제가 더 늦게 발견되기 쉽습니다.

그래서 저는 유령 의존성을 "가끔 생기는 예외적인 버그"라기보다, 설치 모델이 다르면 개발자가 보게 되는 오류의 시점도 달라진다는 예시로 이해하게 되었습니다.

그래서 무엇을 기준으로 고르면 좋을까

이제는 패키지 매니저를 고를 때 "누가 더 빠르다더라"보다 아래 질문을 먼저 보게 됩니다.

1. 호환성을 최우선으로 볼 것인가

생태계 기본값과 가장 넓은 호환성이 중요하다면 npm은 여전히 강한 선택지입니다. 최신 npm도 install strategy와 workspace, 보안 관련 옵션이 꾸준히 강화되고 있어서 과거 이미지로만 볼 이유는 없습니다.

2. 의존성 엄격성을 어디까지 가져갈 것인가

유령 의존성을 가능한 빨리 드러내고 싶다면 Yarn PnP나 pnpm 쪽이 더 강한 인상을 줍니다. 다만 Yarn PnP는 설치 모델이 크게 달라지는 만큼 적응 비용이 더 있을 수 있습니다.

3. 팀의 도구 호환성과 학습 비용을 감당할 수 있는가

좋은 패키지 매니저를 고르는 일은 항상 "이상적인 구조"만의 문제는 아닙니다. 팀의 빌드 도구, CI, IDE, 기존 레포 구조와 얼마나 자연스럽게 맞물리는지도 함께 봐야 합니다. 같은 도구라도 작은 단일 프로젝트와 큰 workspace에서는 체감이 다릅니다.

지금은 왜 pnpm을 더 자주 쓰는가

지금의 저는 pnpm을 가장 자주 씁니다. 이유는 단순히 빠르기 때문만은 아닙니다.

  • node_modules 호환성을 크게 버리지 않습니다.
  • 디스크 사용과 설치 속도 면에서 이점이 있습니다.
  • workspace와 monorepo 흐름에서 강점이 분명합니다.
  • 의존성 경계를 예전보다 더 엄격하게 볼 수 있습니다.

무엇보다 중요한 것은, 이제는 "팀이 쓰니까 그냥 쓴다"가 아니라 왜 이 도구가 우리 구조에 맞는지 설명할 수 있다는 점입니다. 예전에는 Yarn이든 pnpm이든 빠르다는 말만 듣고 옮겨 갔습니다. 지금은 적어도 설치 모델과 의존성 해석 방식을 기준으로 선택하고 있습니다.

마치며

패키지 매니저를 이해하는 일은 명령어를 외우는 일보다, 의존성이 디스크에 어떤 구조로 놓이고 어떤 규칙으로 해석되는지를 이해하는 일에 더 가깝다고 느끼고 있습니다.

그래서 npm, Yarn, pnpm의 차이도 "누가 더 낫다"로 정리하기보다, 각 도구가 어떤 시대의 문제를 풀려고 했는지로 보는 편이 더 도움이 됩니다. npm은 표준축을 넓히고 있고, Yarn은 설치 모델과 보안 모델을 적극적으로 바꿔 왔으며, pnpm은 호환성을 유지하면서 더 엄격하고 효율적인 구조를 만들려 했습니다.

다음에는 이 흐름을 그대로 이어서, 왜 package manager 이야기가 결국 workspace와 monorepo 구조 이야기로 연결되는지도 정리해 보려고 합니다.

참고