Val Town처럼 의존성이 많은 React 앱을 운영하며 package.json을 깔끔하게 유지하는 방법을 정리했습니다. 새 의존성의 소스 읽기, npm/pnpm 트리와 잠금파일 이해, 번들/디스크 크기 분석, 좋은/나쁜 모듈을 가르는 기준, Renovate·Knip 활용, 신뢰할 저자 목록까지 실전 팁을 담았습니다.
Val Town은 엄청나게 많은 의존성을 가진 React 애플리케이션입니다. 복잡하고, 우리는 늘 의존성 업그레이드와 씨름합니다. 웹을 과도하게 복잡하게 만드는 최악의 죄를 저지르고 있죠: 이 글을 쓰는 시점 기준으로 우리의 node_modules 디렉터리는 863MB입니다. 휴!
그런데, 정말 그럴까요? 우리는 아무 생각 없이 의존성을 깔아대며 기술 부채를 마구 떠안고 있는 걸까요? 꼭 그렇지는 않다고 봅니다.
문제는, 우리가 만들려는 것에는 필수적인 복잡성이 있다는 겁니다. 우리만의 TypeScript 트랜스파일러를 직접 만들지도 않을 거고, CodeMirror 설치를 피해 텍스트 영역으로 코드 편집을 때우지도 않을 겁니다. 저는 매주 조금씩 package.json을 훑어보며 “여기서 뭘 지울 수 있지?”라고 생각합니다. 때로는 뽑아낼 수 있는 의존성을 찾기도 하지만, 대개 빈손입니다. 실제로 이 모든 잡동사니가 필요하거든요. 원칙이 현실과 맞닿을 때 판단이 어떻게 바뀌는지, 고생하며 배울수록 남을 쉽게 재단하던 태도는 옅어집니다.
그렇다고 의존성 다듬기에 예술이 없다는 건 아닙니다. 여러 기법과 도구가 맞물려 ‘의존성 위생’을 만들어 줍니다. 저는 그동안 이런 방식을 다듬어 왔는데, 전체를 한번에 적어본 적은 없는 것 같습니다. 이번에 정리해 봅니다.
룰 #1은 읽기입니다. 정말 말 그대로예요: 프로젝트에 새로 도입하려는 의존성의 소스 코드를 읽으세요. 물론 README도요. 저는 옛 방식대로 눈과 머리로 읽는 걸 강력히 추천합니다. LLM이 도움이 될 수는 있지만 전부를 떠넘기지는 마세요. 목표는 실제 ‘이해’이고, 그건 대리로 달성할 수 없습니다.
꽤 자주, 이렇게 읽어보면 새로 추가하려던 의존성이 고작 50줄짜리라는 걸 알게 됩니다. 이럴 땐 NPM으로 설치하기보다 벤더링하는 편이 낫습니다. 코드를 복사해 넣고, 오픈소스 라이선스를 코드 주석으로 보존하세요.
또는 모듈이 gzip 기준 2MB에 달하고, 새로운 추이 의존성 3개를 끌어들이는데, 실제로는 그 중 50줄만 쓰게 된다는 사실을 깨달을 수도 있습니다. 이건 좋지 않은 시나리오입니다. 순전한 마이너스인 표면적만 늘어나는 셈이니까요. node_modules가 더 커질 수도 있고, 사용하지 않는 부분에 보안 취약점이 있어도 똑같이 대응해야 할 수도 있습니다.
저는 React 같은 초대형 의존성은 예외로 둡니다. React의 알고리즘과 TypeScript 컴파일러 내부를 들여다본 적이 있는데, 그 분야의 고수들을 신뢰하기로 했습니다.
읽지 않으면, 성공할 수 없습니다.
npm ls와 package-lock.json 읽기는 최고의 친구pnpm을 쓰면 pnpm-lock.yaml과 pnpm why가 있죠. 다른 패키지 매니저에도 비슷한 명령이 있습니다. 이유는 간단합니다. 직접 추가한 의존성은 빙산의 일각일 뿐입니다. node_modules를 진짜로 채우는 건 그들이 끌고 오는 것들, 즉 추이 의존성입니다. 그리고 그건 정말 중요합니다.
예를 들어, 프로젝트가 TypeScript를 트랜스파일해야 한다고 해봅시다. 아마 이미 트랜스파일러가 설치되어 있을 겁니다. 우리 프로젝트에서는 drizzle-kit, Vite, tsx가 저마다 esbuild를 끌어옵니다. 그래서 esbuild를 ‘직접 의존성’으로 추가해도 비용이 없습니다. 모두가 내부적으로 쓰던 같은 esbuild 바이너리로 중복 제거되니까요. 이건 유용한 작은 게임입니다. 애플리케이션을 위해 무언가를 설치할 때, 이미 추이로 설치된 것을 재사용할 방법을 찾으면 공짜로 의존성을 하나 얻는 셈입니다!
그리고 package-lock.json이나 pnpm-lock.yaml을 ‘읽으세요’. 그리 끔찍하지 않고, 분명 배우는 게 있습니다. 거기엔 정보가 아주 많아요. 다른 라이브러리가 무엇에 의존하는지 익숙해지면, 머릿속에 작은 페이지랭크 알고리즘이 생겨서 새로운 게 필요할 때 곧바로 떠올릴 수 있게 됩니다. 호기심을 먹이듯 npmjs.com 페이지들을 열어보세요.
큰 NPM 모듈이 미치는 영향은 두 가지입니다. 분산된 애플리케이션(배포물) 크기에 대한 영향, 그리고 개발 중 node_modules가 차지하는 디스크 용량입니다. 우선은 애플리케이션 크기에 집중하는 게 좋지만 둘 다 중요합니다. node_modules에 2GB의 코드에 의존하는 앱은 CI 테스트가 느리고, 다운로드할 게 많아 배포도 느려집니다.
저는 20년 된 빈티지 앱 Grand Perspective로 디스크 위의 node_modules를 살핍니다. Linux나 Windows를 쓴다면 고려할 만한 디스크 공간 분석기가 많이 있습니다.
배포 애플리케이션의 크기를 분석하는 일은 훨씬 더 복잡하고 시스템에 따라 다릅니다. 우리는 Vite와 React Router를 쓰므로 rollup-plugin-visualizer를 사용합니다. 하지만 번들러마다 다르고, Next.js의 번들 분석기처럼 자체 솔루션이 있는 프레임워크도 있습니다.
그렇다면 무엇을 찾아야 할까요? 좋은 모듈의 정의는 계속 달라지지만, 대체로 유지보수 이력이 준수하고, TypeScript 타입이 내장되어 있고, 테스트가 통과하고, 문서가 좋습니다.
속어로 줄이면 ‘유능함의 바이브’가 있어야 합니다. 당신이 무에서 유를 만들며 실수를 많이 하더라도, 가져다 쓰는 부품만큼은 탄탄해야 합니다. 애플리케이션의 버그는 당신이 쓴 버그와 남에게서 물려받은 버그의 합입니다. 그러니 설치하는 코드가 직접 쓰는 코드보다 더 높은 기준을 요구하는 게 공평하죠.
나쁜 모듈은 뭘까요? 방치되고 엉성한 모듈도 물론 나쁘지만, 더 나쁜 건 ‘틀린 문제’를 푸는 모듈입니다. 실제로 필요한 문제에 맞지 않아, 모듈에 맞추기 위해 문제를 비틀어야 하는 경우죠. 이건 읽고, 당신의 문제와 해결책을 이해하는 데 시간을 조금만 투자하면 피할 수 있습니다. 아니면 LLM에게 물어봐도 됩니다, 애기야.
Renovate를 쓰세요. 모듈을 최신으로 유지하라고 계속 알려줍니다. 이런 일은 1년에 한 번 몰아서 하기보다, 조금씩 꾸준히 하는 게 훨씬 낫습니다.
그리고 Knip을 쓰세요. 그야말로 마법입니다. 엄청 빠르고, 정확합니다. package.json에 적어놨지만 실제로는 쓰지 않는 모듈을 알려줍니다. 오래된 프로젝트 버전에서 남은 잡동사니를 놓치기 쉬운데, Knip으로 치워버리세요! 프로젝트에서 더는 쓰지 않는 파일도 보여줍니다. Knip 티셔츠가 있다면 지금 입고 있을 겁니다. 그 정도로 좋아요.
NPM 생태계는 ‘사람들’로 이뤄져 있습니다. 그 사람들이 누군지 아는 건 유용합니다! 예를 들어, Promise(그리고 많은 다른 주제) 관련 무언가가 필요하면 Sindre Sorhus가 이미 올려둔 게 있는지 확인합니다. 아주 자주 있습니다!
isaacs, Matteo Collina, Mafintosh처럼 탄탄한 작업물을 많이 가진 사람들을 알아두는 것도 좋습니다.
Markdown을 다룬다면 wooorm과 unified 레포지토리를 알아야 합니다. 차세대 Node.js 관련? unjs를 보세요. 트랜스파일러 내부 구현이 궁금하다면, Rich Harris의 프로젝트를 항상 확인하세요. 보석이 잔뜩 있습니다.
다음 글에서는 시작점으로 삼을 만한 의존성 목록만 모아볼 예정입니다. 원한다면 AGENTS.md 같은 파일에 넣어도 좋아요.
이게 진실입니다. 우리는 서로의 어깨 위에 쌓아 올립니다. 다만 어떤 어깨 위에 올릴지 고르는 데는 기술이 있습니다.
어느 정도 답답하긴 합니다. 웹 플랫폼과 NPM 생태계는 너무 빠르게 움직이고, 자잘한 업데이트와 결정을 너무 많이 요구하니까요. 하지만 그게 보통입니다. NPM과 Node의 교훈을 배운 차세대 언어라도 비대한 패키지 생태계의 문제에서 자유롭지 못합니다. 의존성 정원 가꾸기는 일의 일부이고, 우리는 그걸 잘 해내야 합니다.