TL;DR
HOXY...
Node.js 기반 frontend, backend 구현하면서
npm install을 무지성으로 쓰고있나요?
git으로 협업하다가
package-lock.json의 무수한 충돌을 맛본 적 있나요?
이제,
package 관리를 해줄 때입니다...ㅎ
우리 같은 초보를 위한
패키지 관리 룰을 공유합니다!
<기본 룰>
1. 개발자들 간의 npm 버전을 동일하게 맞춘다
2. 패키지 관리 담당자를 정해두고, 그 사람이 프로젝트에 사용되는 패키지 관리(추가, 삭제, 업데이트)를 하도록 한다
<새 패키지를 설치하는 경우>
1. 패키지 관리 담당자가 전용 branch에서 `npm install [package이름]`으로 새 패키지를 설치한다
2. 1에 이어서, 업데이트된 package.json과 package-lock.json의 최신 상태를 commit에 남기고 push한다
3. 해당 branch가 main에 merge된다
4. 나머지 개발자들은 main을 본인 브랜치에 merge하여 새로운 package-lock.json을 받아들인 뒤, `npm ci`로 node_modules를 업데이트 시킨다.
<기존 패키지를 삭제하는 경우>
1. 패키지 관리 담당자가 전용 branch에서 `npm uninstall [package이름]`으로 패키지를 제거한다
2. 1에 이어서, `npm install`를 실행하여 의존적 패키지 및 기타 패키지를 업데이트 해준다.
3. 2에 이어서, 업데이트된 package.json과 package-lock.json의 최신 상태를 commit에 남기고 push한다
4. 해당 branch가 main에 merge된다
5. 나머지 개발자들은 main을 본인 브랜치에 merge하여 새로운 package-lock.json을 받아들인 뒤, `npm ci`로 node_modules를 업데이트 시킨다.
왜 이렇게 하나, 또 이외의 경우는 어떡하나 싶다면..?
본문으로 고고!
1. 문제 상황
- git pull을 했더니 package-lock.json에서 핵폭탄급 충돌이 발생한다.
3년 전,
프론트엔드 개발자로서 처음 일을 시작한 스타트업에는
매달 새로운 인턴 개발자 4명 정도가 와서 한달 동안 일을 하고 떠났다.
그들이 각자의 개발 환경을 세팅하고,
새로운 기능을 추가할 때마다 꼭 발생하는 문제가 있었다.
바로 각자 환경에서 git pull을 하면,
package-lock.json 파일에서 엄청난 충돌이 일어나는 것이었다.
나도 아무런 기반없이
무작정 구현부터 시작한 초짜라서 원인을 몰랐고,
chatGPT도 없던 시절이라,
Stack Overflow와 각종 블로그를 참고하여
`npm install`대신 `npm ci`를 쓰는 것으로
문제를 단순하게 해결했었다.
당시엔 나름 npm 사용법 공부를 열심히 했다고 생각했는데,
지금와서 보니 남에게 설명이 가능한 수준에는 훨씬 못미쳤다.
그리고 지금,
크래프톤 정글에서의 프로젝트 보완 작업을 진행하다가
또 다시 발생한 package-lock.json 충돌 문제를 해결하며
이 문제로부터 졸업을 하고자
포스팅을 남겨본다.
2. 원인 규명을 위한 배경지식 쌓기
2.1. Node.js 환경의 패키지
패키지라는 단어는 다양한 곳에서 쓰인다.
그중 Node.js 환경에서 개발하는 우리가 이해해야 할 패키지의 의미를 알아보자.
패키지(Package)란?
- 대부분 '모듈의 모음'의 이미지를 가짐
- 하지만, 어떤 개발 환경에서 이 단어를 쓰느냐에 따라 개념이 다소 다름
- 예시
- Python 생태계에서의 패키지
- 라이브러리를 구성하는 하위 개념
- 여러 모듈을 조직화한 디렉토리 구조
- Node.js 생태계에서의 패키지
- npm이 규정해놓은 특정 구조를 갖춘 '재사용 가능한 모듈의 모음'
- 개발자들 사이에서는 라이브러리와 혼용하여 사용될 정도로, 거의 동일한 개념임
- Python 생태계에서의 패키지
2.2. 패키지 관리와 npm, yarn, pnpm
앞에서 Node.js 환경의 패키지를 논하다가 갑자기
'npm'이 튀어나왔다.
이는 가장 대표적인 패키지 관리자로,
개발자들이 수많은 패키지를 제작/활용하는데 도움을 주는 툴이다.
이제 이 패키지 관리자 종류 몇가지를 알아보자.
1) npm
- Node Package Manager로, Node.js 생태계가 생겨나던 시점에 만들어진 Package 관리 툴임
- 다양한 Package를 보유한 저장소와 이를 활용할 수 있는 인터페이스를 제공함
- 안 궁금하지만, 궁금한(?) 사실... 2010년 Isaac Z. Schlueter가 개발, 현재 GitHub이 소유중
2) yarn
- npm의 단점을 보완하기 위해 만들어진 패키지 관리 툴
- 병렬 설치와 캐싱 메커니즘을 활용하기에 npm보다 설치가 빠름
- 설치된 패키지가 예상된 것과 정확히 일치하는지 확인하는 무결성 체크섬 방식을 도입하여, 보안성이 더 좋음
- 2016년 Facebook에서 개발
3) pnpm
- Performant npm으로, npm과 yarn의 단점을 보완하기 위해 만들어진 패키지 관리 툴
- 패키지를 중복없이, 전역 저장소에 한 번만 저장하여 디스크 공간 효율을 높임
- 프로젝트의 node_modules에 실제 파일 대신, 전역 저장소에 대한 심볼릭 링크를 생성하니 설치 과정이 크게 축소되어 빠름
- 2017년 Zoltan Kochan이 개발, 아직 독립 프로젝트임
4) npm, yarn, pnpm... 어떨 때 어떤 것을 사용해야 하나?
npm | yarn | pnpm | |
고려사항 | - 초보자 및 소규모 프로젝트 | - 중/고급 개발자 및 대규모 프로젝트 - 보안과 성능이 중요한 팀 |
- 대규모 프로젝트, 특히 모노레포 구조 - 디스크 공간 절약이 중요한 팀 |
2.3. npm의 패키지 관리 방식
보통 우리같은 초보는
영문도 모른 채 npm으로 패키지 관리에 입문한다.
무지성으로 사용하고 있던 npm의 패키지 관리 방식을
차근차근 이해해보자.
1) 패키지 관리의 필요성
- 패키지 그냥 설치해서 가져다 쓰면 되는거 아닌가? No!!
- 패키지 또한 누군가가 만들어서 올려둔 코드 모음일 뿐이기에, 그 안정성을 100% 보장받을 수 없음
- 버그가 있을 수도 있고, 갑자기 업데이트가 되어서 구버전과 호환이 안 될 수도 있고,
- 나는 패키지A를 다운받았는데, 그 패키지A의 내부 동작에서 문제가 있는 패키지B가 쓰인 경우,
나는 영문도 모른 채 대환장 에러 파티에 초대될 수 있음 - 패키지를 다운받느라 네트워크를 너무 많이 사용하거나, 저장공간을 너무 많이 써서 클라우드 서버 이용료 폭탄을 맞을 수 있음
- 이외에도 잠재적 문제가 많음
- 위와 같은 상황을 사전에 예방하기 위해, 패키지 관리는 필수임
- 다행히 패키지 관리자는 패키지 설치와 활용 뿐 아니라, 이런 버전과 안정성, 효율성 관리를 할 수 있는 기능도 제공함
- 앞서 살펴본 것처럼 yarn, pnpm은 npm보다 더 철저한 관리 기능을 제공함
2) npm의 패키지 관리 방식
- 패키지 설치
- `npm install [패키지_이름]`을 실행하면 해당 패키지의 생산자가 설정한 package.json 파일에 따라 패키지가 설치됨
- 프로젝트의 package.json에 해당 패키지 이름과 버전이 업데이트 됨
- 의존성 해결
- 만약 설치한 패키지가 또다른 패키지에 의존하고 있는 경우, 관계되어 있는 다른 패키지들도 모두 함께 설치됨
- 이때, npm은 패키지 간에 호환되는 버전을 탐색하여 설치함
- 버전 관리
- 패키지를 설치하면, 해당 패키지의 버전과 관계되어 있는 다른 패키지의 버전에 대한 정보가
프로젝트의 package-lock.json에 업데이트 됨
- 패키지를 설치하면, 해당 패키지의 버전과 관계되어 있는 다른 패키지의 버전에 대한 정보가
- 패키지 저장 방식
- 해당 패키지와 의존성 패키지가 프로젝트의 node_modules 폴더에 평탄화(flattening)구조로 설치 됨(npm v3 이후)
- 예를들어,
- #case1. 패키지C의 v2에 의존하는 패키지A와 패키지C의 v3에 의존하는 패키지B를 설치한다면,
프로젝트의 node_modules 폴더의 루트에 패키지A, 패키지B와 패키지C의 v3 가 각각 설치되고,
패키지A 내부의 node_modules 폴더에 패키지C의 v2가 설치됨
(동일한 패키지의 경우 최신 버전이 루트에 설치됨) - #case2. 동일하게 패키지C의 v2에 의존하는 패키지A와 패키지B를 설치하면,
프로젝트의 node_modules 폴더의 루트에 패키지A, 패키지B, 패키지C의 v2가 각각 설치됨
- #case1. 패키지C의 v2에 의존하는 패키지A와 패키지C의 v3에 의존하는 패키지B를 설치한다면,
- 패키지 삭제
- `npm uninstall [패키지_이름]`을 실행하면, 해당 패키지가 node_modules 폴더에서 삭제됨
- 이에 따라 package.json, package-lock.json에서 관련 정보가 삭제됨
- 해당 패키지에만 의존하던 패키지들은 삭제되지 않으며, `npm prune`으로 정리해야 함
3. 원인 규명과 해결 방안 도출
이제 패키지와 npm을 충분히 이해했으니, 문제를 해결해보자.
3.1. package-lock.json 충돌의 원인
- 한줄 요약 === 무지성 패키지 관리
- `npm install` 명령어 실행시 어떤 작업이 실시되는지 정확하게 모른 채 사용
- package-lock.json이 어떨 때 변경되는지 모른 채 commit 내용에 업데이트 내용 추가
- 문제 상황
- 시간의 경과
- 프로젝트에 필요한 패키지를 마지막으로 업데이트하고 1개월이 지난 뒤,
팀원A가 본인의 작업 브랜치에서 `npm install [패키지명]`을 실행하여 새로운 패키지를 업데이트함 - 또 다시 1개월이 지난 뒤, 해당 브랜치가 메인 브랜치로 merge됨
- 프로젝트에 필요한 패키지를 마지막으로 업데이트하고 1개월이 지난 뒤,
- git push, merge, pull...
- 팀원B가 메인 브랜치를 본인 작업 브랜치로 merge 받고, `npm install`을 실행하여 패키지를 업데이트 함
- 팀원B가 본인의 작업 브랜치를 push함. 이때 기능 관련 코드만 추가되었고, 새로운 패키지 추가는 없음
- 팀원B의 브랜치가 메인 브랜치로 merge됨
- conflict 발생
- 팀원A가 메인 브랜치를 본인의 작업 브랜치로 merge받았더니 package-lock.json에서 수많은 충돌이 일어남
- 우리 눈으로 하나하나 비교해가며 conflict를 해결하기에는 너무 버거움
- 시간의 경과
- 원인
- 기존 패키지의 변화
- 시간이 지남에 따라 우리 프로젝트에서 사용했던 많은 패키지들이 제작자에 의해 업데이트된 상태
- package.json에 버전범위가 지정된 상태에서, `npm install` 사용
- 특정 패키지 이름을 명시하지 않고 `npm install`을 실행하면,
package.json에 명시된 버전 범위 안에서 업데이트가 된 패키지는 재설치 됨
- 특정 패키지 이름을 명시하지 않고 `npm install`을 실행하면,
- package-lock.json 파일의 변경
- 팀원B가 본인의 작업 브랜치에서 `npm install`을 실행했을 때,
팀원A가 새로 추가한 패키지 뿐 아니라, 다른 패키지의 업데이트 상태가 반영되어 패키지가 대거 재설치 됨 - 그 결과로 팀원B의 package-lock.json 파일이 대거 변경됨
- 팀원B가 본인의 작업 브랜치에서 `npm install`을 실행했을 때,
- package-lock.json 파일의 버전 차이 발생
- 그러나 팀원A는 `npm install [특정 패키지명]`을 실행하여, 해당 패키지만을 추가하였기에
package-lock.json 파일의 변동이 크지 않았음 - 팀원B의 브랜치가 메인에 merge된 시점에,
팀원A의 브랜치에는 package-lock.json은 너무 오래전 버전이 남겨져 있었음 - 따라서 package-lock.json에서 엄청난 충돌이 일어난것
- 그러나 팀원A는 `npm install [특정 패키지명]`을 실행하여, 해당 패키지만을 추가하였기에
- 기존 패키지의 변화
3.2. 핵심 문제에 대응할 방책: 패키지 담당자와 npm ci
- npm 제대로 사용하기
- npm 버전 일원화
- `npm install`, `npm install [패키지명]`, `npm ci` 등 npm 명령어의 수행 작업 숙지하기
- 팀내 패키지 관리 담당자 설정
- 패키지 설치 전후, 안정성 검토 및 관리
- 패키지 브랜치를 관리하여 패키지 업데이트 현황을 구성원과 명확히 공유
- 주기적인 브랜치 최신화
- 나머지 개발자의 경우, 다량의 충돌을 사전에 방지하기 위해 작업 브랜치 최신화를 자주 해주기
4. 해결 방안의 적용
4.1. npm 버전 일원화
- Node.js와 npm 버전을 명시해두고, 확인할 수 있도록 함
- 프로젝트 중간에 누군가 합류하거나, 기존 멤버라도 개발환경을 다시 셋팅하는 경우에도 철저히 버전을 일원화하기 위함
// package.json
{
"name": "project-name",
"version": "1.0.0",
// ⬇⬇⬇ 바로 이 부분!!!!
"engines": {
"node": "16.x",
"npm": "8.x"
},
"dependencies": {
// 프로젝트 의존성 리스트
},
}
4.2. case 별 실행 방안
패키지 관리자 - #case1. 새로운 패키지 설치
- 해당 패키지를 활용할 예정인 기능 개발 담당자와 함께 패키지 안정성을 검토
- 패키지 전용 브랜치에서 패키지 설치한 뒤 commit 남기기
git checkout -b package-management
npm install <new-package>
git add package.json package-lock.json
git commit -m "Add <new-package>"
git push origin package-management
# PR 생성 및 리뷰 후 머지
패키지 관리자 - #case2. 기존 패키지 삭제
- 삭제할 패키지 및 관계된 의존 패키지 사용 현황 확인
- 패키지 전용 브랜치에서 패키지 삭제하고, 패키지 전반 의존성 업데이트 한 뒤 commit 남기기
git checkout -b package-management
npm uninstall <package-to-remove>
npm install
git add package.json package-lock.json
git commit -m "Remove <package-to-remove>"
git push origin package-management
# PR 생성 및 리뷰 후 머지
나머지 개발자 - #case 1, 2 모두
- 본인 브랜치에 작업 내용 commit하거나 stash시키기
- 메인 브랜치를 pull 받고, package-lock.json에서 충돌이 발생하면
main 브랜치의 내용을 받아들이기 - `npm ci`로 패키지 설치
- 여기서 ci는 'clean install'의 약자로 node_modules 폴더를 완전히 삭제한 뒤,
package-lock.json 파일만을 기반으로 패키지들을 다시 설치하는 작업임
- 여기서 ci는 'clean install'의 약자로 node_modules 폴더를 완전히 삭제한 뒤,
# 본인 브랜치에서 작업 내용 commit 혹은 stash 처리
git checkout main
git pull upstream main
git checkout my-branch
git merge main
npm ci
5. 고민해 볼 과제
5.1. 패키지 안정성 관리
- 위의 해결 방안은 패키지 안정성을 따로 관리하지 않는 문제가 있음
- 패키지 관리자나 누군가가 패키지들의 안정성을 주기적으로 확인하며, 문제가 있는 경우 해결해야 함
- `npm audit`을 실행하면, 패키지들의 현황을 검토할 수 있음
5.2. 의존성 범위 설정
- 위의 해결 방안은 패키지 제작자가 정해놓은 패키지 버전 범위를 그대로 두고 따름
- 패키지를 어느 수준까지 자동 업데이트시킬 것인지 능동적으로 고려하여 설정하는 것이 좋음
- SemVer에 따른 버전 범위 지정자(^, ~, >, < 등)를 참고하기
5.3. 모노레포와 패키지 관리
- 대규모 서비스를 운영하는 경우, 프로젝트별로 공유할 수 있는 자원이 많아 모노레포 방식으로 개발 효율을 높인다고 함
- 모노레포는 한 저장소에 엄청나게 많은 프로젝트가 존재하기 때문에 디스크 용량과 로드 및 실행 속도 관리가 무척 중요함
- 그런데 npm은 두가지 측면에서 모두 비효율적이어서, 모노레포 방식으로 운영하는 서비스에서 최악의 패키지 관리 방식임
- 따라서, 큰 서비스를 운영하는 경우에 어떤 것이 개선되어야 하는지 알아두면 좋음
5.4. CI/CD와 패키지 관리
- 배포를 자동으로 실행하는 스크립트인 만큼 앱 안정성을 위해 패키지 관리에 필요한 코드가 필수적임
- 사실 `npm ci`는 CI/CD 과정에서 자주 쓰이는 명령어! (그치만 그 ci와 이 CI는 다른 뜻임)
'Web Development > Front-end' 카테고리의 다른 글
[Web Workers] AI 모델을 브라우저의 백그라운드에서 실행하면 앱 성능이 좋아질까? (1) | 2024.10.01 |
---|---|
[React] 미세한 DOM 조작 효과를 화면에 즉각 반영하고 싶을 때, useEffect vs useLayoutEffect 어떤 것을 써야할까? (0) | 2024.09.04 |
[Semantic Web/Tag] 웹을 가치있는 정보로 만들기 (0) | 2021.12.29 |