JavaScript 비대화의 세 가지 축
(43081j.com)- 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) 지원
-
전역 네임스페이스 변조 방지
- cross-realm 값 처리
-
오래된 엔진 지원
- ES3 환경에는
Array.prototype.forEach,Object.keys,Object.defineProperty등 ES5 기능이 존재하지 않음 - 이런 환경에서는 직접 구현하거나 polyfill을 사용해야 함
- 가장 좋은 해결책은 업그레이드이지만, 일부 사용자는 여전히 구버전을 유지함
- ES3 환경에는
-
전역 네임스페이스 변조 방지
- 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 호환성을 위해 존재함
- iframe 간 객체 전달 시
-
문제점
- 대부분의 개발자는 현대 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 같은 프로젝트가 문서화·도구화를 통해 이를 지원 중이며, 각 개발자도 자신의 의존성을 점검하고 “왜 필요한가?”를 질문해야 함
- 모두가 함께 정리할 수 있음
Hacker News 의견들
-
요즘은 의존성 없는 JavaScript로 개발하는 게 가장 좋은 방향이라 생각함
JS/CSS 표준 라이브러리도 훌륭하고, 정적 분석(TypeScript의 JSDoc 체크), ES 모듈, 웹 컴포넌트 등도 충분히 강력함
사람들은 이 방식이 확장성이나 유지보수에 불리하다고 하지만, 내 경험상 오히려 단순하고 변경이 쉬운 구조를 유지할 수 있었음- 나도 몇 년째 이 접근을 실험 중이며, plainvanillaweb.com이라는 튜토리얼 사이트도 만들었음
프레임워크나 빌드 도구가 하는 일 대부분은 브라우저 내장 기능과 바닐라 패턴으로 대체 가능함
다만 이런 방식은 아직 생소한 영역이라, 대부분의 튜토리얼 생태계가 대형 프레임워크 중심으로 돌아가는 게 문제임
실제로 React 코드를 완전 바닐라로 옮겨도 모듈성은 유지되고 코드 길이는 약 1.5배 정도 늘 뿐, 의존성이 없어서 성능은 오히려 좋아짐
물론 의존성이 나쁘다는 건 아님. 다만 많은 개발자가 “반드시 써야 한다”는 고정관념에 갇혀 있음 - 단순한 마케팅 페이지라면 가능하지만, 기능이 많은 앱이라면 의존성이 필수적인 경우가 많음
예를 들어 나는 지도 기능이 많은 사이트를 만드는데, mapbox/maplibre/openlayers 같은 대안 없는 라이브러리를 써야 함 - 2022년에 이 방식으로 프로젝트를 진행했는데, CVE나 버전 마이그레이션 관련 문제는 전혀 없었음
클라이언트도 마이그레이션 비용을 한 푼도 내지 않았음 - 컴포넌트 렌더링은 쉽지만, 프레임워크의 핵심은 모델의 반응형 업데이트 제공임
이 글처럼 모델 업데이트를 어떻게 처리하는지 궁금함 - 20년 가까이 JS를 다뤄왔는데, 결국 최소한의 의존성만 두고 나머지는 직접 만드는 방식으로 정착했음
오히려 대규모 코드베이스를 적은 인원으로 유지하기가 더 편해졌음
요즘 도구들 덕분에 예전보다 직접 구현이 훨씬 쉬워졌고, agentic engineering과도 잘 맞음
- 나도 몇 년째 이 접근을 실험 중이며, plainvanillaweb.com이라는 튜토리얼 사이트도 만들었음
-
글이 잘 쓰였고, 감정적이지 않으면서 문제를 명확히 설명함
JS가 “표준 라이브러리”를 제대로 갖추지 못한 게 이런 상황의 일부 원인이라 생각함- 요즘 JS 표준 라이브러리가 꽤 방대해졌는데, 아직 어떤 기능이 부족하다고 보는지 궁금함
- 나는 오히려 격한 글(rant) 을 좋아함. 사람들의 감정뿐 아니라 그 이유를 이해하는 데 도움이 됨
-
좋은 글이지만, 문제의 근본은 불필요한 추가(=bloat) 자체라고 생각함
“완벽함은 더 이상 추가할 게 없을 때가 아니라, 더 이상 뺄 게 없을 때 이루어진다”는 생텍쥐페리의 말을 인용하고 싶음
대부분의 소프트웨어는 “어떻게 더 우아하게 만들까?”보다 “어떻게 더 쉽게 추가할까?”를 묻는 식으로 작성됨
답은 언제나npm i more-stuff임- 커트 보니것의 글쓰기 규칙처럼, “모든 문장은 캐릭터를 드러내거나 행동을 전진시켜야 한다”는 원칙을 떠올림
데모스테네스와 키케로의 대비처럼, 덜어낼 수 없는 코드가 좋은 코드임 - 모든 소프트웨어에 불필요한 부분이 있지만, 특히 npm 패키지와 웹앱이 심함
JS는 과거와 미래 브라우저 호환성을 모두 고려해야 하고, UI 중심 언어라 접근성·국제화·모바일 지원 등으로 부피가 커짐
- 커트 보니것의 글쓰기 규칙처럼, “모든 문장은 캐릭터를 드러내거나 행동을 전진시켜야 한다”는 원칙을 떠올림
-
많은 경우 이건 숨겨진 기술 부채 문제로 보임
컴파일 타깃을 ESx로 안 올리고, 패키지나 구현을 업데이트하지 않는 게 원인임
ES5는 이미 13년째 모든 브라우저에서 지원됨 (caniuse.com/es5)- 실제로는 오래된 JS 엔진을 지원하려는 사람과, 수많은 미니 패키지를 만드는 사람이 있음
둘 다 자신들의 행동을 기능이라 여기고, 인기 패키지를 많이 유지함
그래서 바뀌기 어려움. 가끔 커뮤니티가 비판하지만, 그들도 나름 논리를 갖고 있음 - ES6 이하 호환성을 유지하려는 욕심이 이상하게 느껴짐
Babel로 구버전으로 트랜스파일하면 코드가 비대하고 느려지며, 정작 옛 브라우저에서는 CSS나 JS 기능 한계로 안 돌아감
심지어 polyfill이 문제를 일으킨 적도 있었음 (BigInt를 처리 못한 지수 연산자 polyfill) - 오래된 브라우저뿐 아니라 이상한 브라우저 지원도 필요함
콘솔, TV, 구형 안드로이드, iPod touch, Facebook 내장 브라우저 등 다양한 환경이 존재함
그래서 외부 모듈 하나만 두고, 나머지는 트랜스파일러 설정으로 해결함 - 웹은 “지금 배포하고 나중에 고치자”는 문화가 강해서, 낡은 의존성이 오래 남음
- Angular의 설계 결정처럼, 과거의 구조가 현재의 비효율을 초래하는 경우도 있음
예전엔 비동기 추적을 위해 setTimeout 등을 오버라이드했지만, 이제는 signals로 훨씬 단순하게 처리 가능함
- 실제로는 오래된 JS 엔진을 지원하려는 사람과, 수많은 미니 패키지를 만드는 사람이 있음
-
일부 패키지 저자들이 다운로드 수를 늘리기 위해 의존 트리를 인위적으로 쪼갠다고 생각함
7줄짜리 패키지가 존재하는 건 말이 안 됨. lockfile 메타데이터가 코드보다 큼
예전에 create-react-app 의존성 중 5%가 한 저자의 미니 패키지였음
has-symbols, is-string, ljharb 같은 사례가 있음- 이런 행위가 단순한 자존심인지, 실제로 이익이 있는지 궁금함
예를 들어 Anthropic은 npm 다운로드 수가 많은 오픈소스 유지보수자에게 무료 Claude를 제공함 - immich.app/cursed-knowledge 글처럼, “하위 호환성”을 이유로 50개 패키지를 추가하는 사람도 있음
- 보안 측면에서도 심각함. 이런 마이크로 패키지 하나하나가 공격 표면이 됨
다운로드 수 경쟁은 오히려 위험을 키움 - 예전에 어떤 사람이 조직에 침투해 자기 패키지를 의존성에 추가해 이력서용 스타 수를 올린 사례도 있었음
- 문화적 문제도 있음. JS 커뮤니티에서는 7줄짜리 코드를 직접 붙여 넣는 걸 “바퀴 재발명”이라 비판함
하지만 다른 문화에서는 그게 오히려 좋은 일로 여겨짐
- 이런 행위가 단순한 자존심인지, 실제로 이익이 있는지 궁금함
-
JS 생태계를 비판하기 전에 30 years of br tags를 읽어보면 좋음
JS와 도구의 진화 과정을 이해할 수 있음
단순히 “JS 개발자들이 문제다”라고 말하는 건 공학적 사고의 결여임- 나쁜 현실을 이해하려는 태도는 좋지만, 과도한 수용은 패배주의임
우리는 항상 더 나은 이론과 실천을 고민해야 함
소프트웨어 세계는 빠르게 변하므로, 스스로 “가짜 장례식”을 치르며 낡은 관행을 버릴 필요가 있음 - 이 글은 개발자를 비난하기보다, 현 상태를 합리적으로 비판하는 글로 느껴졌음
- 나쁜 현실을 이해하려는 태도는 좋지만, 과도한 수용은 패배주의임
-
9년 된 Node.js 코드베이스를 관리 중인데, 의존성은 8개뿐이고 모두 하위 의존성 없음
Node 내장 기능을 우선 활용하고, 필요한 부분만 직접 구현함
예전보다 훨씬 안정적이고 스트레스가 적음
Deno의 표준 라이브러리도 훌륭해서, 런타임 기본 기능과 함께라면 몇 개 패키지로도 충분히 앱을 만들 수 있음
JS는 신중하게 접근하면 꽤 괜찮은 언어임 -
is-string같은 패키지의 cross-realm 안전성 주장은 이해하지만, 실제로 그런 상황은 드묾
npm이 너무 쉽게 배포를 허용하면서 “모듈을 쪼개서 배포하자”는 철학이 과잉 확장된 게 문제임
소비자는 의존 트리를 감사하지 않고 그냥 설치하므로, 선택적 비용이 기본 비용이 되어버림
ponyfill 문제는 자동화로 해결 가능함
예를 들어 Node LTS 버전에서 이미 지원되는 기능을 자동 감지해 제거하는 Renovate 스타일 봇이 도움이 될 것 같음 -
사내 PWA의 원칙은 단 하나임:
“Chrome 최신 버전으로 업그레이드하라. 그래도 문제가 있으면 그때 본다”- 내부용이라면 이게 맞는 접근임. 회사가 지원 브라우저를 정하면 관리가 쉬움
Safari가 메모리를 덜 쓰는 건 이해하지만, 정책적으로 통일하는 게 효율적임 - 단순하게 유지하는 게 결국 가장 큰 이득을 줌
- 내부용이라면 이게 맞는 접근임. 회사가 지원 브라우저를 정하면 관리가 쉬움
-
“ES3(IE6/7 수준)까지 지원해야 한다”는 말은 정말 이해하기 힘듦
보안상 은행 사이트조차 그런 구형 브라우저를 막아야 함- 이런 팀은 대부분 2015년쯤 세팅한 빌드 도구를 아직도 안 바꾼 경우임
Webpack, Babel, polyfill 스택을 업그레이드하는 건 큰 일이라 그냥 그대로 둠
“고장 나지 않았으면 고치지 말자”식 문화임 - 실제로 그런 구버전 지원을 주장하는 특정 인물이 있고, 그가 많은 저수준 패키지를 유지함
- 참고로 Deutsche Bahn이 아직 Windows 3.1을 쓴다는 이야기도 들었음
- 이런 팀은 대부분 2015년쯤 세팅한 빌드 도구를 아직도 안 바꾼 경우임