- npm 생태계에서 의존성 트리의 비대화가 주요 문제로 지적되며, 이는 오래된 런타임 지원, 원자적 패키지 구조, 오래된 ponyfill 사용에서 비롯됨
- 구형 엔진 호환성과 cross-realm 안전성을 이유로 유지되는 작은 유틸리티 패키지들이 현대 환경에서도 불필요하게 남아 있음
-
원자적 아키텍처는 재사용성을 높이려 했으나, 실제로는 중복·보안·유지보수 비용을 증가시키는 비효율적 구조로 작용함
- 이미 모든 엔진이 지원하는 기능을 위한 낡은 ponyfill 패키지들이 제거되지 않아 불필요한 다운로드와 관리 부담을 초래함
- 커뮤니티는 e18e, knip, module-replacements 같은 도구를 통해 불필요한 의존성 정리와 네이티브 기능 전환을 추진 중임
JavaScript 의존성 비대화의 세 가지 축
-
e18e 커뮤니티의 성장과 함께 성능 중심의 기여가 늘어나며, 불필요하거나 유지되지 않는 패키지를 정리하는 cleanup 활동이 진행 중임
- npm 생태계에서 의존성 트리의 비대화(dependency bloat) 가 주요 문제로 지적되며, 오래된 런타임 지원, 원자적 패키지 구조, 오래된 ponyfill 사용이 핵심 원인으로 꼽힘
1. 오래된 런타임 지원 (안전성과 realm 포함)
- npm 트리에는 is-string, hasown 같은 작은 유틸리티 패키지들이 다수 존재하며, 이는 다음 세 가지 이유로 유지됨
- 매우 오래된 엔진(예: ES3, IE6/7, 초기 Node.js) 지원
-
전역 네임스페이스 변조 방지
-
오래된 엔진 지원
- ES3 환경에는 Array.prototype.forEach, Object.keys, Object.defineProperty 등 ES5 기능이 존재하지 않음
- 이런 환경에서는 직접 구현하거나 polyfill을 사용해야 함
- 가장 좋은 해결책은 업그레이드이지만, 일부 사용자는 여전히 구버전을 유지함
-
전역 네임스페이스 변조 방지
- Node는 내부적으로 primordials 개념을 사용해 전역 객체를 초기 시점에 래핑하여 변조로부터 보호함
- 예를 들어 Map을 재정의하면 Node 자체가 깨질 수 있으므로, Node는 원본 참조를 유지함
- 일부 패키지 유지자는 이 방식을 일반 패키지에도 적용해 math-intrinsics 같은 안전성 중심 패키지를 사용함
-
Cross-realm 값
- iframe 간 객체 전달 시 instanceof 검사가 실패하는 문제 발생
- 예: window.RegExp !== iframeWindow.RegExp
-
chai 같은 테스트 프레임워크는 Object.prototype.toString.call(val) 방식으로 realm 간 타입 검사를 수행함
-
is-string 같은 패키지는 이런 cross-realm 호환성을 위해 존재함
-
문제점
- 대부분의 개발자는 현대 Node나 evergreen 브라우저를 사용하므로 이런 호환성이 불필요함
- 그러나 이들 패키지가 일반 의존성 트리의 “핫패스”에 포함되어 모두가 비용을 지불하게 됨
2. 원자적(Atomic) 아키텍처
- 일부 개발자는 패키지를 가능한 한 작은 단위로 분리해 재사용 가능한 빌딩 블록으로 구성해야 한다고 주장함
- 결과적으로 shebang-regex, arrify, slash, path-key, onetime, is-wsl 등 극도로 세분화된 패키지들이 다수 존재함
- 예: shebang-regex는 단 한 줄의 정규식만 포함 (/^#!(.*)/)
-
문제점
- 대부분의 원자적 패키지는 재사용되지 않거나 단일 소비자만 존재
- 예:
-
shebang-regex → shebang-command만 사용
-
cli-boxes → boxen, ink만 사용
-
onetime → restore-cursor만 사용
- 이런 경우 인라인 코드와 동일하지만 npm 요청, 압축 해제, 대역폭 등의 추가 비용이 발생
-
중복 문제
- 예: nuxt@4.4.2 의존성 트리에서 is-docker, is-stream, is-wsl, path-key 등이 2개 버전씩 중복 존재
- 인라인 코드로 대체하면 버전 충돌이나 해상 비용이 사라져 중복 비용이 거의 없음
-
공급망 위험 확대
- 패키지 수가 많을수록 보안·유지보수 리스크 증가
- 실제로 한 유지자가 다수의 작은 패키지를 관리하다 계정이 해킹되어 수백 개 패키지가 동시에 손상된 사례 존재
- 단순한 코드(Array.isArray(val) ? val : [val])는 별도 패키지로 둘 필요 없이 인라인으로 처리 가능
-
결론
- 원자적 아키텍처는 의도와 달리 비효율적이고 위험한 구조로 변질
- 대부분의 사용자에게 실질적 이득 없이 전체 생태계가 비용을 부담
3. 오래된 Ponyfill
-
Polyfill은 엔진이 지원하지 않는 기능을 환경에 추가하는 코드이며,
Ponyfill은 환경을 수정하지 않고 직접 import하여 사용하는 대체 구현임
- 예: @fastly/performance-observer-polyfill은 polyfill과 ponyfill을 모두 제공
-
문제점
- Ponyfill은 과거에는 유용했으나, 대상 기능이 이미 모든 엔진에서 지원됨에도 제거되지 않음
- 예시:
-
globalthis (2019년부터 지원, 주간 4,900만 다운로드)
-
indexof (2010년부터 지원, 주간 230만 다운로드)
-
object.entries (2017년부터 지원, 주간 3,500만 다운로드)
- 이런 패키지는 대부분 단순히 제거되지 않았기 때문에 남아 있음
- 모든 LTS 엔진이 기능을 지원하면 ponyfill은 제거되어야 함
비대화 해소 방안
-
의존성 트리의 깊은 중첩으로 인해 정리 작업은 어렵지만, 커뮤니티 협력으로 개선 가능
- 각 개발자는 “이 패키지가 정말 필요한가?”를 자문하고, 불필요하다면 이슈를 제기하거나 대체 패키지 탐색 필요
-
module-replacements 프로젝트는 네이티브 기능으로 대체 가능한 패키지 목록을 제공
-
knip 사용
-
knip은 사용되지 않는 의존성 및 죽은 코드 탐지 도구
- 직접적인 해결책은 아니지만, 정리의 출발점으로 유용
-
e18e CLI 활용
-
@e18e/cli analyze 명령으로 대체 가능한 의존성 탐지 가능
- 예: chalk → picocolors로 자동 마이그레이션
- 향후에는 환경에 따라 Node의 styleText 같은 네이티브 기능 추천 예정
-
npmgraph 활용
-
npmgraph.js.org는 의존성 트리 시각화 도구
- 예: eslint@10.1.0 트리에서 find-up 브랜치가 고립되어 있음
- 단순한 파일 탐색 기능에 6개 패키지가 필요하지 않으므로, empathic 같은 더 작은 대안 사용 가능
-
module replacements 프로젝트
- 커뮤니티가 대체 가능한 패키지와 네이티브 기능 매핑 데이터셋을 유지
-
codemods 프로젝트를 통해 자동 마이그레이션도 지원
결론
- 현재의 비대화는 소수의 구형 호환성·특이한 구조를 유지하려는 사용자 때문에 전체가 비용을 부담하는 구조
- 과거에는 불가피했지만, 현대 엔진과 API가 충분히 발전한 지금은 불필요한 부담
- 앞으로는 이 소수가 별도의 스택을 유지하고, 나머지는 가볍고 현대적인 코드 기반을 사용하는 방향으로 전환 필요
-
e18e와 npmx 같은 프로젝트가 문서화·도구화를 통해 이를 지원 중이며,
각 개발자도 자신의 의존성을 점검하고 “왜 필요한가?”를 질문해야 함
-
모두가 함께 정리할 수 있음