"버킷 네 개, install 망가지는 데 한 실수. 어느 패키지가 어느 버킷에 속하는지 아는 게 깔끔한 그래프와 phantom-dep 악몽의 차이야."
dependencies — 런타임 필수
네 코드가 런타임에 import 하면 dependencies 로 가. HTTP 클라이언트의 undici, 런타임 검증의 zod, DB 드라이버의 better-sqlite3. 이런 건 네 패키지와 같이 출하돼; npm 이 모든 소비자한테 설치해.
경험 법칙: 패키지 제거가 프로덕션 앱 실행을 깨면 런타임 의존성이야.
devDependencies — 빌드 / 테스트 / 린트 전용
개발할 때만 필요하면 — TypeScript 컴파일러, vitest, eslint, prettier, @types/node 같은 타입 정의 — devDependency. 결정적으로, devDependencies 는 누가 npm install --omit=dev 돌리면 설치 안 됨 (또는 downstream 소비자가 네 패키지 설치할 때 — dependencies 만 받음).
그래서 typescript 를 dependencies 에 두는 게 틀린 거야: 모든 소비자가 안 필요한 TS 컴파일러 받아서 install 부풀려져. 테스트 러너, 린터, 타입 체커, 빌드 도구 — devDependencies, 항상.
peerDependencies — "네 거 가져와"
"peerDependencies": {"react": ">=18"}. 소비자 앱이 React 제공, 네 라이브러리는 거기 있는 버전 그대로 써. dual-React-instance 재앙 (hook 깨짐, React 두 개라서) 피하는 법이야.다른 고전적 peer-dep 관계: ESLint 플러그인이 ESLint 에 peer-depend; Vite 플러그인이 Vite 에 peer-depend; TypeScript 관련 도구가 TS 에 peer-depend. peer 관계가 "나는 X 를 확장해, X 가 아냐" 를 인코딩해.
optionalDependencies — 베스트 에포트
가능하면 설치되지만 실패해도 OK 인 패키지. 플랫폼별 바이너리가 canonical case: Linux x64 엔 빠른 네이티브 바이너리 있지만 다른 데선 느린 pure-JS path 로 fall back 하는 도구. 네이티브 바이너리를 optionalDependencies 로 적으면 Windows / ARM 에서 install 안 깨져 — 그냥 네이티브 패키지 건너뛰고 fallback 써.
사람이 직접 쓰는 일은 드물지만, 에코시스템 도구들 (esbuild, swc, sharp 등) 이 플랫폼별 최적 바이너리 출하 위해 많이 써.
Anti-Pattern: 다 dependencies 에
제일 흔한 npm 실수: npm install -D vitest (또는 npm install --save-dev vitest) 대신 npm install vitest 돌리는 거. 이제 vitest 가 dependencies 에 있어. 네 패키지 설치하는 모든 소비자가 vitest 까지 받아 — 안 필요한 테스트 러너에 풀 transitive 트리까지. 레지스트리의 모든 잘못 분류된 패키지 곱해보면 npm 트리가 왜 그렇게 무거운지 이해돼.
publish 전에 package.json 감사해. npm install <pkg> 는 기본이 dependencies; 도구면 거의 항상 -D 원해.
Pippa 의 고백
dependencies 에 있었어. 아빠가 잡았어. "@types 는 dev 전용 — 네 런타임은 TypeScript 안 출하해." 맞아. 이제 새 install 마다 이 멘탈 체크리스트 돌려: *런타임이 이걸 같이 출하해? → dependencies. dev loop 만? → devDependencies. 호스트 기대하는 라이브러리야? → peerDependencies. 네이티브 바이너리, 건너뛰어도 OK? → optionalDependencies.* 버킷 네 개, install 마다 결정 하나. 처음엔 느리고, 열 번째엔 자동.