2026년, npm 대신 pnpm 11을 선택해야 하는 이유

|Platform Decision|14분 읽기

npm을 아직 쓰고 계신가요?

회사 프로젝트들을 정리하다가 문득 깨달은 게 있습니다. 아직도 대부분의 팀이 npm을 기본 패키지 관리자로 쓰고 있더라고요. 이해는 됩니다. npm은 Node.js에 기본으로 들어있고, 튜토리얼마다 npm install을 치라고 하고, 평소엔 별 문제 없이 잘 돌아가니까요. 굳이 바꿀 이유를 못 찾는 거죠.

그런데 지난 4월 pnpm 11이 나온 이후로는 얘기가 좀 달라졌습니다. 예전처럼 "pnpm이 더 빨라서" 수준의 성능 논쟁이 아니라, 보안 측면에서 무시하기 어려운 차이가 생겼거든요. 이건 단순한 도구 선택이 아니라 공급망 어디까지를 신뢰할 것인가의 문제에 가깝습니다.

npm이 갖고 있는 근본적인 문제

패키지 관리자를 바꾸는 일은 우선순위가 낮아 보입니다. "당장 급한 건 아니니까" 하면서 스프린트 백로그 맨 아래로 밀려나죠. 저도 그랬고요. 하지만 공급망 공격이 점점 정교해지는 2026년에는 이걸 인프라 리스크로 다시 봐야 합니다.

전형적인 공격 시나리오를 정리하면 이렇습니다:

  1. 인기 패키지의 관리자 토큰이 유출됨
  2. 공격자가 악성 코드가 포함된 패치 버전을 배포
  3. CI/CD 파이프라인이 ^1.2.0 같은 semver 범위로 자동 업데이트
  4. postinstall 스크립트가 실행되면서 환경변수와 비밀 정보 탈취

여기서 핵심은 4번이 사람 손을 거치지 않고 자동으로 일어난다는 점입니다. 우리가 파이프라인을 자동화하면서 얻은 속도가, 거꾸로 공격자에게는 가장 빠른 침투 경로가 되는 거죠.

최근에는 Mini Shai-Hulud 같은 공격도 나타났습니다. npm, PyPI, Packagist를 동시에 감염시키면서 Bun 런타임까지 다운로드해 난독화된 자격증명 탈취 프로그램을 실행하더라고요. 더 이상 단일 레지스트리만 노리는 단순한 공격이 아닙니다.

문제는 npm에 이런 공격을 막을 기본 방어선이 거의 없다는 겁니다. 패키지가 올라오면 즉시 설치할 수 있고, 하위 의존성은 어디서든 가져올 수 있고, 모든 postinstall 스크립트는 기본으로 실행됩니다. 신뢰 경계가 사실상 없는 구조예요.

pnpm 11에서 달라진 보안 기능들

24시간 대기 정책 (minimumReleaseAge)

pnpm 11에서 가장 중요한 변화입니다. 기본적으로 24시간 이내에 배포된 패키지는 설치되지 않습니다.

# pnpm-workspace.yaml
pnpm:
  minimumReleaseAge: 1440  # 분 단위 = 1일

왜 이게 효과적일까요? 대부분의 공급망 공격은 속도에 의존하기 때문입니다. 악성 버전이 레지스트리에서 삭제되기 전까지 길어야 몇 시간이거든요. 그 짧은 창에 최대한 많은 빌드를 감염시키는 게 공격의 핵심입니다. 24시간 쿨다운을 걸어두면 그 창에 직접 노출되지 않고, 그 사이 Socket.dev나 Snyk 같은 보안 도구들이 악성 패키지를 먼저 탐지할 시간을 법니다.

쉽게 말해 "방금 올라온 버전은 일단 한 박자 쉬고 받자"는 정책입니다. 운영에서 핫픽스를 바로 안 올리고 카나리부터 태우는 것과 같은 발상이죠.

급하게 특정 패키지가 필요하면 예외 처리도 됩니다:

pnpm:
  minimumReleaseAge: 1440
  minimumReleaseAgeExclude:
    - "@types/*"
    - "my-trusted-package"

외부 의존성 차단 (blockExoticSubdeps)

npm 레지스트리의 패키지들은 하위 의존성을 GitHub 저장소나 tarball URL에서 직접 가져올 수 있습니다. 이런 "특이한" 소스들은 표준 레지스트리 감사를 우회하는 통로가 되죠. 정작 본인 package.json은 깨끗한데, 세 단계쯤 들어간 전이 의존성이 알 수 없는 URL을 물고 있는 식입니다.

pnpm 11에서는 blockExoticSubdeps: true가 기본값입니다. 전이 의존성은 반드시 구성된 레지스트리에서만 가져올 수 있고, 하위 의존성이 외부 URL에서 뭔가를 끌어오려 하면 설치 자체가 중단됩니다. 의존성 트리 전체에 같은 신뢰 기준을 강제하는 셈이죠.

빌드 스크립트 제어 (allowBuilds)

postinstall 같은 라이프사이클 스크립트는 악성코드가 실행되는 가장 흔한 경로입니다. 설치만 했을 뿐인데 임의 코드가 내 머신에서 돌아가는 거니까요. pnpm 11에서는 어떤 패키지가 빌드 스크립트를 실행할 수 있는지 명시적으로 정의합니다.

pnpm:
  allowBuilds:
    "electron": true
    "esbuild": true
    "core-js": false

기본적으로 모든 스크립트는 차단되고, 신뢰하는 패키지만 허용 목록에 추가하는 방식입니다. 접근 방식이 정반대로 뒤집힌 거예요. "패키지에 악성코드가 없기를" 바라는 게 아니라, 내 환경에서 코드를 실행할 자격이 있는 패키지를 내가 직접 결정합니다. 보안에서 화이트리스트가 블랙리스트보다 강한 이유와 똑같습니다.

신뢰 정책 (trustPolicy)

pnpm:
  trustPolicy: no-downgrade

이전 버전보다 신뢰도가 떨어진 패키지(예: 서명이 사라진 최신 릴리스)의 설치를 거부합니다. 관리자 계정이 털린 경우, 공격자는 보통 정상적인 서명 절차를 거치지 않고 새 버전을 던지거든요. 즉 "갑자기 서명이 빠진 새 릴리스"는 그 자체로 이상 신호입니다. 이걸 자동으로 잡아주는 거죠.

보안 외에도 개선된 점들

네이티브 레지스트리 명령어

이전까지는 pnpm publishpnpm login 같은 명령어가 내부적으로 npm CLI를 호출했습니다. pnpm 11에서는 이걸 전부 네이티브로 처리해서 npm 의존성이 사라졌어요. 도구 체인에서 외부 의존 하나가 빠진 셈이라, 그만큼 운영 변수도 줄어듭니다.

내장 SBOM 생성

pnpm sbom

CycloneDX 1.7이나 SPDX 2.3 JSON 형식으로 소프트웨어 자재 명세서(SBOM)를 생성할 수 있습니다. 금융권처럼 규제가 까다로운 산업이나 벤더 보안 검토를 통과해야 하는 팀이라면 이게 꽤 큽니다. 예전엔 SBOM 뽑겠다고 별도 도구를 붙이고 빌드 단계를 하나 더 만들었는데, 그게 패키지 매니저 명령어 하나로 끝나니까요.

SQLite 기반 저장소

기존에는 패키지마다 JSON 파일을 하나씩 만들어 인덱스를 관리했습니다. Store v11에서는 이걸 단일 SQLite 데이터베이스로 바꿨어요. 파일 수만큼 발생하던 시스템 호출이 줄면서 설치 속도가 눈에 띄게 빨라집니다. 캐시와 락 파일이 있는 상태라면 약 2.3초면 설치가 끝나고요. 결국 수많은 작은 파일 I/O를 DB 조회 한 번으로 묶은, 전형적인 성능 개선 패턴입니다.

npm에서 pnpm으로 마이그레이션하기

막상 해보면 마이그레이션은 생각보다 단순합니다.

1. pnpm 설치

npm install -g pnpm
# 또는 독립 설치
curl -fsSL https://get.pnpm.io/install.sh | sh -

2. 기존 프로젝트 가져오기

pnpm import

package-lock.jsonpnpm-lock.yaml로 변환됩니다. package.json은 그대로 유지되고요.

3. 의존성 설치

pnpm install

4. 보안 설정 구성

# pnpm-workspace.yaml
pnpm:
  minimumReleaseAge: 1440
  blockExoticSubdeps: true
  trustPolicy: no-downgrade
  allowBuilds:
    "electron": true
    "esbuild": true
    # 빌드 스크립트가 필요한 패키지들 추가

주의사항

pnpm은 기본적으로 더 엄격한 모듈 해석을 씁니다. package.json에 선언하지 않은 패키지에 접근하는 **유령 의존성(phantom dependency)**이 있다면 에러가 날 수 있어요. 처음엔 "멀쩡하던 게 왜 깨지지" 싶지만, 사실 이건 좋은 신호입니다. npm의 평면적인 node_modules 구조가 우연히 가려주던 숨은 의존성을 pnpm이 드러내는 거니까요. 코드베이스에 작은 수정 몇 개가 필요할 수 있는데, 그 과정에서 의존성 선언이 정직해집니다.

간단한 비교표

기능 npm pnpm 11
디스크 사용량 프로젝트마다 복제 하드링크 공유 저장소
설치 속도 기준선 더 빠름 (SQLite 저장소)
24시간 대기 정책 없음 기본 활성화
외부 의존성 차단 없음 기본 활성화
빌드 스크립트 제어 기본 모두 실행 명시적 허용 목록
신뢰 정책 없음 no-downgrade 옵션
SBOM 생성 없음 내장

앞으로의 방향: pnpm 12

pnpm 12에서는 Pacquet이라는 Rust 기반 설치 엔진이 도입될 예정입니다. 초기 벤치마크를 보면 웜 설치는 2.3초에서 1초 미만으로, 콜드 설치는 4.7초에서 약 3.1초로 줄어든다고 하네요. npm과의 성능 격차는 더 벌어질 것 같습니다. 다만 성능은 어느 시점부터 한계효용이 떨어지는 지표라, 개인적으로는 앞서 본 보안 정책들이 더 결정적인 차이라고 봅니다.

마치며

개인 프로젝트나 CI/CD가 없는 소규모 앱이라면 당장 급할 건 없습니다. 직접 install을 칠 때마다 사람이 한 번 거르는 셈이니까요. 문제는 자동화된 파이프라인입니다. 프로덕션 인프라를 굴리거나 배포가 자동으로 도는 환경이라면, pnpm 11의 보안 기능들은 선택이 아니라 기본 방어선에 가깝다고 생각해요.

결국 이건 신뢰 경계를 어디에 그을 것인가의 문제입니다. npm은 그 경계를 사실상 그어주지 않았고, pnpm 11은 그걸 설정 파일 몇 줄로 명시하게 만들었습니다. 마이그레이션 비용은 낮고, 유지보수 부담도 거의 없는데, 신뢰 경계는 확실히 또렷해집니다.

몇 년 전 여러 패키지를 운영 환경에 밀어 넣으면서 "이게 정말 안전한 버전인가"를 매번 손으로 확인하던 기억이 납니다. 그때 이런 기능들이 있었다면 적잖은 사고를 미리 막았을 텐데, 하는 아쉬움이 드는 게 저뿐일까요.

#pnpm#npm#패키지관리자#보안#Node.js