나란히 있다고 같은 일은 아니에요
이 줄이 내 패키지를 향하는지 남의 패키지를 향하는지만 보면 돼요.
모노레포 설정을 정리하다가 pnpm-workspace.yaml 을 열면 catalog 와 packageExtensions 가 위아래로 붙어 있어요. 둘 다 의존성 버전을 적는 자리라 처음엔 비슷한 기능인 줄 알았거든요. 근데 이 둘은 반대편을 봐요. catalog 는 내 패키지들이 쓸 버전 표기를 모으고, packageExtensions 는 남의 패키지가 잘못 적어둔 선언을 고칩니다.
같은 파일에 있어서 생긴 오해
pnpm 설정이 한곳에 모인 건 얼마 안 됐어요. 예전엔 .npmrc 와 package.json 의 pnpm 필드에 흩어져 있었는데, pnpm 10.6 부터 pnpm-workspace.yaml 이 그 설정들을 camelCase 키로 받기 시작했어요. 그리고 pnpm 11 부터는 package.json 의 pnpm 필드를 아예 읽지 않아요.
그래서 지금은 워크스페이스 루트의 이 파일 하나에 전부 나란히 놓입니다.
# pnpm-workspace.yaml
packages:
- packages/*
catalog:
react: ^18.3.1
overrides:
"@acme/core": "3.28.0"
packageExtensions:
react-redux:
peerDependencies:
react-dom: "*"들여쓰기 레벨이 같으니 사촌쯤 되는 기능들로 보이죠. 근데 각각이 건드리는 대상은 완전히 달라요.
packageExtensions 가 고치는 건 남의 선언이에요
어떤 플러그인을 깔았는데 eslint 를 못 찾겠다며 터진 적 있으실 거예요. 그 플러그인이 실제로는 eslint 를 필요로 하면서 peerDependencies 에 안 적어둔 경우가 흔해요. npm 처럼 의존성을 최상위로 끌어올려 평평하게 까는 방식(hoisting)에서는 우연히 옆에 깔린 eslint 를 집어서 조용히 넘어가는데, pnpm 은 선언한 패키지만 최상위에 노출하니까 그 자리에서 바로 드러나요. 이 구조가 왜 이렇게 갈라져 왔는지는 npm 말고도 많은 이유에서 다뤘어요.
이때 hoisting 을 켜서 덮어버리는 대신, 그 패키지의 package.json, 그러니까 매니페스트를 설치 시점에 보완해주는 게 packageExtensions 예요.
# pnpm-workspace.yaml
packageExtensions:
fork-ts-checker-webpack-plugin:
dependencies:
"@babel/core": "1"
peerDependencies:
eslint: ">= 6"
peerDependenciesMeta:
eslint:
optional: true
express@1:
optionalDependencies:
typescript: "2"키는 패키지 이름만 적어서 모든 버전에 걸거나, express@1 처럼 버전 범위를 붙여 특정 버전대만 골라 패치해요. 확장할 수 있는 필드는 dependencies, optionalDependencies, peerDependencies, peerDependenciesMeta 네 개고요.
"Together with Yarn, we maintain a database of packageExtensions to patch broken packages in the ecosystem." - pnpm Settings
pnpm 과 Yarn 이 같은 목록을 공유해서, 이미 알려진 깨진 패키지들은 기본으로 패치된 상태예요. 그래서 내 저장소에 한 줄 적고 덮어두기보다 @yarnpkg/extensions 로 올려서 다 같이 덕을 보는 게 원래 의도예요.
packageExtensions 가 만지는 건 선언뿐이에요. 패키지 안의 코드는 한 글자도 안 바뀌어요.
catalog 가 모으는 건 내 쪽 표기예요
catalog 는 방향이 반대예요. 남의 패키지가 아니라 내가 관리하는 워크스페이스 패키지들이 참조할 버전 범위를 한곳에 적어두는 기능이거든요.
"Catalogs are a workspace feature for defining dependency version ranges as reusable constants." - pnpm Catalogs
"상수" 라는 단어가 핵심이에요. 버전 숫자를 package.json 마다 흩뿌리지 않고 이름 하나로 부르게 만드는 거죠.
# pnpm-workspace.yaml
catalog:
redux: ^5.0.1
catalogs:
react17:
react: ^17.0.2
react18:
react: ^18.3.1// packages/dashboard/package.json
{
"dependencies": {
"react": "catalog:react18",
"redux": "catalog:"
}
}단수 catalog 가 기본 카탈로그고, 복수 catalogs 아래에는 이름을 붙여 여러 벌을 둘 수 있어요. 참조할 때 기본 카탈로그는 catalog: 만 적고, 이름 붙은 쪽은 catalog:react18 처럼 이름을 붙여요. 쓸 수 있는 자리는 package.json 의 네 가지 의존성 필드와 pnpm-workspace.yaml 의 overrides 뿐이에요.
게시할 때는 catalog: 가 실제 범위로 치환돼서 나가요. workspace: 프로토콜과 같은 방식이라, 바깥에서 npm 으로 설치하는 사람은 이 저장소에 카탈로그가 있었다는 것도 모르고 그냥 설치돼요.
한 가지 덜 알려진 건 pnpm add 가 새 의존성을 카탈로그에 자동으로 넣어주지 않는다는 거예요. catalogMode 기본값이 manual 이라서요. 팀 전체에 카탈로그를 강제하려면 strict 로 올리고, 카탈로그에 없는 버전은 그냥 직접 의존성으로 떨어뜨리고 싶으면 prefer 를 쓰면 돼요. 버전이 갈려서 같은 패키지가 두 벌 설치되는 걸 catalog 로 막는 실전 순서는 core 가 두 벌이 된 날에 정리해 뒀어요.
경계선은 안쪽이냐 바깥쪽이냐
헷갈릴 때는 고치려는 대상이 내 쪽인지 남의 쪽인지부터 갈라보면 편해요. catalog 만 안쪽을 봐요. 범위를 어디에 적을지를 옮기는 거지, 설치 시점에 새 강제력을 만들지는 않아요. overrides 처럼 그래프를 강제로 갈아끼우는 것과 갈리는 지점이 여기예요.
나머지 셋은 전부 바깥을 봅니다. overrides 는 그래프에 실제로 설치될 버전을 강제로 갈아끼우고, packageExtensions 는 남이 적어둔 선언을 보완해요. 선언으로도 안 되면 pnpm patch 로 패키지 안의 코드를 직접 뜯어고치는 자리가 마지막에 남고요.
실무에서 제일 자주 부딪히는 조합은 overrides 와 packageExtensions 예요. 둘 다 남의 패키지를 건드리니까요. 판단은 의외로 간단해요. 버전 숫자가 마음에 안 들면 overrides, 선언 자체가 빠져 있으면 packageExtensions 입니다.
쓰기 전에 걸리는 것들
packageExtensions 를 건드리면 lockfile 도 같이 움직여요. pnpm-lock.yaml 안에 packageExtensionsChecksum 필드가 있어서 설정 내용을 해시로 들고 있거든요. 이 체크섬은 항목 순서까지 반영해서, 내용이 똑같아도 키 순서만 바뀌면 값이 달라져요. 그래서 packageExtensions 를 손본 커밋에는 lockfile 변경이 따라와야 해요. lockfile 을 빼놓고 --frozen-lockfile 로 도는 CI 에 올리면 거기서 걸리고요.
catalog 쪽 제약은 더 단순해요. 이름 그대로 워크스페이스 기능이라 packages 가 잡혀 있는 저장소에서만 씁니다. 패키지 하나짜리 저장소에서는 catalog: 를 적을 자리가 아예 없어요.
두 키가 헷갈렸던 건 이름이나 문법 때문이 아니라 같은 파일에 나란히 있어서였어요. 저도 한동안 overrides 옆에 붙은 packageExtensions 를 버전 고정하는 다른 문법쯤으로 넘겼거든요. 이 줄이 내 패키지를 향하는지 남의 패키지를 향하는지만 먼저 보면, 남는 건 문법 확인뿐이에요.
자주 묻는 질문
답을 펼치기 전에 스스로 답해보세요
참고 자료
- pnpm - Settings
packageExtensions 로 확장 가능한 네 필드와 키 형식, overrides 및 catalogMode 설명
- pnpm - Catalogs
기본 카탈로그와 이름 붙은 카탈로그 문법, catalog: 프로토콜의 게시 시 치환 동작
- pnpm - pnpm patch
패키지 소스를 직접 고쳐 patchedDependencies 로 등록하는 흐름
- pnpm - Migrating from v10 to v11
package.json 의 pnpm 필드를 더 이상 읽지 않고 pnpm-workspace.yaml 로 모으는 변경
- pnpm/pnpm - packageExtensionsChecksum ordering issue
lockfile 의 packageExtensionsChecksum 이 항목 순서까지 반영하는 동작