원문 : https://cpojer.net/posts/dependency-managers-dont-manage-your-dependencies
기술에 대한 글을 쓰기로 약속했으니 이제 자바스크립트 인프라 구축을 시작해 보겠습니다. 앞으로 몇 개의 블로그 게시물에서는 의존성 관리, 인프라 개선을 위한 실행 가능한 팁, 자바스크립트 인프라 구축을 위한 가이드를 다룰 예정입니다. 자바스크립트 인프라에 대해 더 깊이 있게 이야기할 수 있도록 천천히 이해를 넓혀가겠습니다.
듣기 또는 보기 중 어떤 것을 선호하시나요?
Apple Podcasts, Pocket Casts 그리고 다른 곳들을 통해서도 들을 수 있습니다.
npm, Yarn 또는 pnpm과 같은 자바스크립트 의존성 관리자는 자바스크립트 생태계의 발전에 엄청난 영향을 미쳤습니다. 이것들은 프런트엔드 생태계에서 우리는 새로운 솔루션을 끊임없이 찾을 수 있도록 해줍니다. 그러나 사용 편의성과 패키지의 높은 모듈화에는 많은 단점이 있습니다. 기존의 자바스크립트 의존성 관리자는 실제로 의존성을 관리하는 데 그다지 능숙하지 않습니다. 대신, 주로 아티팩트를 다운로드하고 추출하는 데 편리한 도구에 몇 가지 태스크 러너 기능을 추가한 정도입니다.
이 글은 패키지 관리자의 근본적인 문제를 해결하지는 않습니다. 대신, 하드 드라이브의 블랙홀1을 피하고 타사 의존성을 제어할 수 있는 확실한 가이드를 제공하고자 합니다.
1)
현재 방문하고 있는 웹사이트의 node_modules 폴더에는 40,000개 이상의 파일이 있으며, 그 무게는 570 MiB가 넘습니다 😬
많은 도구가 node_modules
폴더에 있는 파일 분석, 구문 분석, 처리 또는 어떤 작업을 수행합니다. 대규모 의존성을 많이 추가하면 설치 시간이 크게 느려지고2 개인이 프로젝트에서 일부만 사용하더라도 전 세계 모든 사용자의 작업이 느려지는 경향이 있습니다. 아래 방법을 적용하자 페이스북의 리액트 네이티브 코드베이스 내에서 서드파티 의존성의 크기가 크게 줄어들었고 설치 시간도 동일한 수준으로 개선되었으며 이러한 성과를 지속적으로 유지할 수 있었습니다.
2)
제 경험에 따르면 의존성 그래프의 복잡성이 증가함에 따라 설치 시간은 선형 속도 이상으로 증가합니다. 패키지가 더 크더라도 패키지 수가 적으면 설치 시간이 더 빨라집니다.
의존성에 대한 소유권을 갖게 되면 어떤 모습일까요? 개발자 경험의 원칙에 대해 이전 게시물에서 다음과 같이 논의하였습니다.
[하나의] 예시로는 외부 의존성을 줄이거나 더 신중하게 선택하는 것입니다. 물론 서드파티 의존성은 의심할 여지없이 처음에는 더 빠른 개발에 도움이 되지만, 이를 사용하면 개발 스택에 대한 통제력을 잃게 됩니다. 의존성을 제거하거나 적극적으로 유지 보수함으로써 옵션의 가치와 제어권을 얻습니다.
구체적으로 다음과 같이 표시됩니다.
이 가이드는 주로 Yarn 1에 중점을 두고 있지만 많은 권장 사항은 다른 패키지 관리자에게도 동일하게 적용됩니다. 한 가지 명심해야 할 점은 일부 패키지 관리자는 의존성을 직접 설치하지 않기 위해 심볼릭 링크, 하드 링크 또는 기타 까다롭고 불투명한 방법을 사용한다는 것입니다. 저도 그 중 몇 가지를 시도해 보았고 또 다른 패키지 관리자를 만드는 데 참여했습니다. 또한 이러한 솔루션으로 인해 많은 사람들이 고통받는 것을 목격했습니다. 퍼스트파티 코드가 파일 시스템과 버전 관리에 저장되는 것처럼 파일 시스템의 구체화된 파일이 유일한 방법이라는 점을 당연하게 생각합시다. 그럼 시작해보겠습니다!
먼저 node_modules
폴더에 어떤 종류의 패키지가 있는지 파악해야 합니다. 다양한 방법을 사용하여 의존성을 분석하겠습니다. node_modules
폴더를 분석하기 위한 좋은 올인원 솔루션을 찾지 못했기 때문에 대신 네 가지 다른 솔루션을 사용하겠습니다.
du -sh ./node_modules/* | sort -nr | grep '\dM.*'
3을 사용하여 node_modules
의 의존성 크기를 분석합니다.yarn why <packagename>
: 이 명령은 특정 패키지가 의존성 트리에 포함된 이유를 설명합니다. 해당 패키지에 종속된 다른 모든 패키지와 해당 패키지가 의존하는 버전을 나열합니다.3)
shell 구성에서 aliasing을 고려할 때 편리한 명령어 입니다.
다음은 node_modules
폴더를 분석하는 디스크 인벤토리 X의 예시 입니다.4
4)
프리즈마는 훌륭하지만 동일한 큰 바이너리를 세 개의 폴더에 복제하면 안 될까요?
당연해 보일 수도 있지만 많은 프로젝트에는 많은 부담이 따릅니다. 의존성을 제거하는 것보다 추가하는 것이 훨씬 쉽습니다. 사용하지 않는 의존성이 여러 개 있을 수 있으며 사용하지 않더라도 의존성을 계속 업그레이드할 수도 있습니다. package.json
파일에 나열된 기존 의존성을 모두 검사해 제거할 의존성을 확인하는 것이 좋습니다. 정적 분석 도구는 일부 동적 사용 사례를 놓칠 수 있으므로 이 분석은 일반적으로 수동 프로세스입니다. ripgrep을 사용하거나 에디터의 검색 기능을 사용하는 것이 좋습니다. moment.js에서 date-fns로의 마이그레이션이 완료되었는지 확인하여 moment
를 제거하고 싶다고 가정해 보겠습니다. 터미널에서 자바스크립트 require
또는 import/export
문에 대해 다음과 같이 실행할 수 있습니다.
rg '(require\(|from\s+)(?:"|\')moment'
위의 방법으로 결과가 표시되지 않으면 일반적으로 rg 'moment'
를 통해 정확한 문자열을 확인하고 내용이 누락되었는지 검사합니다. Babel, eslint 또는 Jest와 같은 도구의 일부 구성 파일은 모듈 이름을 직접 사용할 수 있습니다. 이러한 의존성 중 하나를 제거하면 프로젝트의 일부가 손상될 수 있습니다.
패키지가 사용 중이 아닌 것을 확인한 뒤 package.json
에서 패키지를 제거하고 dependencies
및 devDependencies
의 각 패키지에 대해 yarn
을 실행하는 프로세스를 반복하세요. npx depcheck
를 통해 더욱 자동화된 방식으로 이 작업을 수행하려면 [depcheck](https://github.com/depcheck/depcheck)
와 같은 도구를 사용할 수 있지만 동일한 주의 사항이 적용됩니다.
"업그레이드 절벽"을 경험해 본 적이 있나요? 의존성 버전이 너무 뒤처져 있어 업그레이드에 상당한 노력이 필요하며 전체 엔지니어링 팀의 속도가 느려지는 경우입니다. 저는 업그레이드를 모든 사람의 지속적인 책임의 일부로 만들면 이 비용을 시간의 흐름에 따라 엔지니어링 조직 전체에 걸쳐 분산시킬 수 있다는 사실을 발견했습니다. 이 접근 방식을 사용하면 사람들이 평균적으로 약간 느리게 움직이기는 하지만, 급격한 변화를 극복하기가 너무 어려워서 막히는 일은 없습니다.
모든 의존성을 최신 상태로 유지하면 레거시 패키지에서 벗어나 이 가이드의 후속 단계를 더 쉽게 수행할 수 있습니다. yarn outdated
및 yarn upgrade-interactive
를 사용하여 의존성을 확인하고 최신 버전5으로 업그레이드할 수 있습니다. 그러나 변경 사항과 버그에 유의하세요. 자동화된 테스트를 통해 신뢰도를 높일수록 최신 버전의 의존성을 유지할 수 있습니다. 변경 사항을 이해하지 못한 채 최신 버전을 사용하기 위해 패키지를 업그레이드했다가 프로덕션에 문제를 일으키는 것만큼 나쁜 일은 없습니다.
5)
깃허브에서 Dependabot을 사용할 수도 있지만, 제 경험상 사람들이 수동으로 변경 사항을 검증하는 데 소요하는 시간이 훨씬 적습니다.
예를 들어, 저는 페이스북에서 수백만 줄의 코드가 포함된 리액트 네이티브의 모든 제품 코드에 대해 새로운 프로덕션 번들을 이전 버전과 비교하는 시스템을 구축하여 Babel의 릴리스를 면밀히 추적하곤 했습니다. 덕분에 자신 있게 업데이트를 출시할 수 있었고, 어떤 경우에는 출시 후 8시간 만에 10억 명의 사용자에게 새 버전의 Babel을 배포할 수 있었습니다.
일부 자바스크립트 패키지 관리자에서 사용되는 알고리즘은 의존성 그래프를 지속적으로 최적화하지 않습니다. 단일 버전만이 예상되는 시맨틱 버저닝(셈버) 범위를 동일하게 충족하는데도 동일한 패키지의 여러 버전을 설치해야 하는 lock 파일이 있을 수 있습니다. yarn-deduplicate
는 이러한 상황에서 lock 파일을 최적화하는 데 사용할 수 있으며 일반적으로 패키지가 추가, 업데이트 또는 제거될 때마다 npx yarn-deduplicate yarn.lock
를 실행하는 것이 좋습니다. 또한 지속적 통합(CI) 파이프라인에 yarn-deduplicate yarn.lock --list --fail
과 같은 확인 단계를 추가할 수도 있습니다.
이 문제를 가장 많이 일으키는 도구는 Babel이나 Jest와 같은 모노레포 도구 체인입니다. 저는 네 가지 버전의 Babel 파서, 두 가지 버전의 거의 모든 Babel 플러그인, 세 가지 버전의 다양한 Jest 패키지가 있는 프로젝트에서 일했습니다. yarn-deduplicate
는 어느 정도 효과가 있었지만 최신 버전을 사용하도록 모든 패키지를 업데이트할 좋은 방법이 없었습니다. 제가 시도한 항목들은 다음과 같습니다.
package.json
파일의 모든 항목을 수동으로 업데이트yarn upgrade
또는 yarn upgrade-interactive
사용이 중 어느 것도 제대로 작동하지 않았고 종종 상황을 악화시켰습니다. 프로젝트에 각 Babel 패키지의 버전이 하나만 설치되도록 하는 신뢰할 수 있는 방법은 단 한 가지뿐이었습니다. Babel을 업그레이드할 때마다 @babel/
로 시작하는 yarn.lock
파일에서 모든 항목을 수동으로 제거하여 Yarn이 의존성 그래프의 하위 집합에 대해 다시 시작할 수 있습니다. 각 Babel 패키지는 최신 버전6인 단일 버전으로 제공됩니다.
6)
물론 특정 버전이 필요한 의존성이 있는 경우를 제외하고는 이러한 의존성을 먼저 업그레이드하거나 Yarn resolutions를 사용하여 제약 조건을 덮어쓰는 것이 좋습니다.
또 다른 제안은 의존하는 패키지의 셈버 범위를 탐색하고 풀 리퀘스트를 보내 해당 의존성을 업그레이드하거나 서버 범위를 허용하도록 고정된 버전을 변경하여 다운스트림 프로젝트가 더 많은 중복 제거의 이점을 누릴 수 있도록 하는 것입니다.
대규모 프로젝트에서는 동일한 용도의 패키지가 여러 개 있을 수 있으며, 때로는 동일한 패키지의 주요 버전이 여러 개 있을 수도 있습니다. 규모가 큰 팀에서는 이미 유사하고 작은 패키지가 널리 사용되고 있는데도 누군가가 대규모 의존성을 가져와서 정확히 한 번만 사용하고 프로덕션 번들의 크기를 부풀리는 경우가 있다는 것을 발견했습니다. 엄격한 스타일 가이드, 문서 및 코드 리뷰를 사용하면 이 문제를 피할 수 있습니다. 하지만 최상의 환경에서도 직접 의존성을 통해 포함된 유사한 전이 의존성으로 인해 문제가 더욱 악화될 수 있습니다. 예를 들어, 두 개의 직접 의존성에는 프로젝트에서 사용 중인 것과 다른 커맨드 라인 옵션을 구문 분석하기 위해 동일한 패키지가 포함될 수 있습니다. 어떤 패키지가 이미 node_modules
에 있는지 분석하고 각 목적에 맞게 단일 패키지에 정렬하는 것이 좋습니다.
어떤 경우에는 패키지가 유지 보수되지 않거나 너무 덩치가 커지거나 너무 느리게 움직이는 경우가 있습니다. 사용 중인 패키지에 대한 서드파티 오픈소스 관리자의 릴리스를 기다린다는 이유로 회사에서 제품 기능을 차단하는 것은 합리적이지 않습니다. 사람들이 패키지를 포크하는 것을 주저하는 것 같았습니다. GitHub의 포크 버튼을 좀 더 자유롭게 사용하여 사용자 지정 버전의 패키지를 게시하는 것이 좋습니다. 포크가 오래 지속될 필요는 없습니다. 이 기능은 어떤 문제에 대한 수정 사항을 적용하는 등의 단일 용도로만 사용되며 나중에 제거할 수 있습니다. 물론 유지 보수 부담이 늘어날 수 있지만 프로젝트에서 실행 중인 코드7를 더 많이 제어할 수 있다는 장점도 있습니다.
7)
아주 작은 변경 사항의 경우,[patch-package](https://github.com/ds300/patch-package)
를 대신 사용할 수 있습니다.
Yarn resolutions를 사용하여 기존 패키지를 사용자 정의 포크로 교체할 수 있습니다. package.json
은 다음과 같습니다.
"resolutions": {
"bloated-package": "npm:@nkzw/not-bloated-package",
"unmaintained-package": "npm:@nkzw/well-maintained-package"
}
이전에는 약 2MiB만 필요했는데 20MiB의 자료를 제공하는 인기 있는 패키지로 이 작업을 수행했습니다. 이 패키지는 세 개의 사본이 있었습니다. 단일 버전으로 정렬하고 포크로 이동하면서 차지하는 공간이 60MiB에서 2MiB로 줄었습니다.
일반적으로 패키지를 포크한 후에는 원래 패키지에 풀 리퀘스트를 보냅니다. 예를 들어, 이번 remark-prism 개선으로 디스크에 저장되는 패키지의 크기가 10.7 MiB에서 0.03 MiB로 줄어듭니다. 때로는 변경 사항이 패키지 소유자와 상충될 수 있으며, 이 경우 포크된 버전을 유지하겠지만 괜찮습니다.
의존성의 개수와 크기를 한 번에 줄이는 것도 좋지만, 장기적으로 그 성과를 유지하는 것이 가장 좋습니다. 프로젝트 내에서 yarn.lock
또는 package.json
파일이 변경될 때마다 du -sh node_modules
와 같은 명령어를 사용하여 node_modules
의 크기를 분석하는 CI 단계를 설정하는 것을 권장합니다. 모든 풀 리퀘스트에서 CI 단계를 실행하고, master
에 비해 크기가 커지면 누군가 리뷰하도록 알림을 보내세요.
물론 사람들이 새 모듈을 체크인하지 못하도록 자동화를 구축할 수도 있지만, 저는 사람들과 대화하고 동일한 코드베이스로 작업하는 모든 사람이 책임감을 공유할 때 최상의 결과를 얻을 수 있다는 사실을 발견했습니다. 결국 의존성을 추가해야 할 정당한 이유가 있는지 여부를 알기 어렵고 게이트키퍼가 되고 싶지 않습니다. 대신, 대규모 의존성을 추가하면 모든 사람의 작업 속도가 느려진다는 점을 지적하거나 이미 사용 중인 유사한 패키지를 안내하는 것이 도움이 됩니다. 대부분의 경우, 사람들은 단지 잘 몰랐을 뿐이며 도움을 받으면 고마워할 것입니다.
예를 들어, 최근에 누군가 node_modules
폴더의 크기를 두 배로 늘릴 수 있는 패키지를 몇 개 추가한 경우가 있었습니다. 문제를 지적하고, 왜 이상적이지 않은지 설명하고, 문제를 해결할 수 있는 다른 두세 가지 방법을 제시하는 것만으로도 그 상태로 풀 리퀘스트를 보내지 않을 수 있었습니다. 누군가 100줄의 코드를 추가하면 코드 리뷰 과정에서 해당 코드를 면밀히 리뷰합니다. 누군가 package.json
파일에 한 줄을 추가하여 100MB의 코드를 프로젝트에 끌어오고, 프로덕션 번들을 부풀리고, 툴링 속도를 늦추는 경우, 풀 리퀘스트 자체8에 영향을 미치지 않기 때문에 일반적으로 변경 사항을 그냥 통과시킵니다. 서드파티 의존성 버전 관리에서 직접 확인하면 이 문제를 방지할 수 있습니다.
8)
그리고yarn.lock
변경 사항은 깃허브에서 기본적으로 축소됩니다. 😩
Yarn에는 제외 목록과 일치하는 파일을 자동으로 제거할 수 있는 autoclean
명령이 있습니다. 예제, 테스트, 마크다운 파일 등 프로젝트와 관련이 없는 모든 것을 제거하는 데 사용할 수 있습니다. 이 기능을 사용 설정하려면 yarn autoclean --init
을 실행하고 결과물인 .yarnclean
파일을 버전 관리에서 확인하면 됩니다. 안타깝게도 이 명령은 설치 중에 실행되는 것이 아니라 의존성을 설치한 후에 실행되므로 Yarn을 호출할 때마다 속도가 느려지며, 때로는 몇 초씩 느려집니다. 훌륭한 기능이지만 버전 관리에서 node_modules
를 확인하는 프로젝트에만 사용하는 것을 권장합니다.
위의 제안이 어느 정도 효과가 있을지는 프로젝트와 팀에 따라 다릅니다. 최소한 프로젝트에 대한 제어력을 높이고 서드파티 의존성의 무분별한 증가를 줄일 수 있습니다. 기껏해야 워크플로우의 성능 저하를 방지하고 항상 간결하고 빠른 상태를 유지할 수 있습니다. 다음 단계는 자바스크립트 인프라 다시 생각하기 입니다.
Really great stuff you have shared with us. Keep sharing this kind of valuable post. Be Ball Players