Node.js 모듈 시스템 이해
CommonJS는 Node.js의 첫 번째 내장 모듈 시스템입니다.
- commonJS에서는 로컬 파일 시스템으로부터 모듈을 임포트하기 위해 require.resolve 알고리즘에 의해 node_modules를 순회하면서 모듈의 위치를 찾아냅니다.
- 현 위치의 node_modules에 모듈이 존재하지 않으면 상위 디렉토리의 node_modules 디렉토리에서 모듈을 찾습니다.
- 이러한 require.resolve 알고리즘은 많은 I/O call을 낳기 때문에 런타임에서 node 구동시에 시간이 오래 걸리는 원인이 되기도 합니다.
기존의 package manager는 node_modules에 모듈을 설치합니다.
- node_modules 디렉토리를 생성하는 것은 I/O-heavy operation 이기 때문에 package manager가 딱히 최적화할 방법이 없습니다.
- package manager가 설치하려고 하는 모듈이 이미 설치되어 있더라도 비효율적인건 마찬가지입니다. 왜냐하면, package manager는 node_modules 디렉토리를 일일이 순회하면서 현재 설치된 파일의 버젼을 확인하고 비교하기 때문입니다.
이를 해결하기 위한 방법: Plug'n'Play
package manager는 이미 디스크에 모듈을 설치하였기 때문에 모듈 의존성 트리에 대해서 이미 알고 있습니다.
- 그렇다면 모듈의 위치를 찾는 것이 왜 Node.js 의 책임이어야 할까요?
- '디스크상 모듈의 위치에 대해 인터프리터에게 알려주는 것'과 '여러 버젼의 모듈 간 의존성을 관리하는 것'은 package manager의 일이어야 합니다.
node_modules 가 아닌 .pnp.cjs
- yarn berry(yarn 2.0 이상 버젼)는 모듈 설치시 node_modules 디렉토리가 아닌 .pnp.cjs 파일을 생성합니다.
- .pnp.cjs 파일에는 모듈의 버젼과 저장된 위치, 참조하고 있는 다른 모듈들을 참조 테이블에 모조리 기록해둡니다.
Plug'n'Play의 이점은 다음과 같습니다
- .pnp.js 파일을 찾는 것만으로 node에게 어떤 패키지가 어디에 있는지 바로 말해줄 수 있습니다.
- 새로운 의존성 설치시에도 node_modules를 보고 현재 설치된 파일의 버젼을 확인할 필요가 없습니다.
- .pnp.js 와 .yarn 디렉토리를 레포지토리에 추가한다면, 의존성을 설치할 필요가 없다. 레포지토리를 클론해서 바로 쓰면 됩니다. → zero install 전략
유의사항
- package.json 의 scripts를 실행시키는 것이 아니라 직접 node 를 구동시킨다면,
yarn node
를 실행하여야 합니다. scripts 는 .pnp.cjs 를 runtime dependency 로 등록하지만, 그냥 node 는 그렇지 않습니다. yarn node
로 실행시켜 .pnp.cjs 를 runtime dependency 로 추가하여야 합니다.
yarn node
가 하는 것은 NODE_OPTIONS 환경 변수에 .pnp.cjs 경로가 포함된 —require 옵션을 추가하는 것입니다.
NODE_OPTIONS="--require $(pwd)/.pnp.cjs" node ./server.js
zero install 전략
- 모든 것을 복잡하게 만드는 package manager의 역할을 제거해버려 가능한 프로젝트를 빠르고 안전하게 하자는 철학입니다.
- package manager의 역할은 다음과 같습니다.
- 디스크상 모듈의 위치에 대해 인터프리터에게 알려주는 것
- 여러 버젼의 모듈 간 의존성을 관리하는 것
왜 zero install 철학이 대두되었나요?
- 추후에 yarn 의 버젼이 바뀌어서 모듈을 설치하는데 버그를 낳을 수도 있습니다.
- production 환경이 변경되어서 yarn install 이 임시 디렉토리에 더이상 파일을 작성할 수 없을 수도 있습니다.
- 네트워크가 실패하거나, 자격 증명(credentials)가 바뀌어서 인증 오류에 직면할 수도 있습니다.
이러한 이슈를 피하는 유일한 방법은 가능한 한 package manager를 조금 사용하는 것입니다.
어떻게 적용할 수 있나요?
.yarn/cache
디렉토리를 remote repository에 추가합니다.
- yarn berry는 모듈 설치시 binary 파일로 압축하여
.yarn/cache
에 저장합니다. 다음번에 yarn install 을 실행시킬 경우 .yarn/cache 에 이미 모듈이 있다면, 재설치하지 않습니다.
.pnp.cjs
파일을 remote repository에 추가합니다.
node_modules를 remote repository에 추가하는 것과 뭐가 다르죠?
- 매우 다릅니다.
- 1.2GB의 node_modules 디렉토리를 yarn berry는 139MB의 바이너리 파일로 저장합니다. Git은 1.2GB는 감당할 수 없지만, 139MB는 충분히 감당할 수 있습니다.
- yarn classic에서 패키지를 하나 업데이트하려고 하면 매우 많은 양의 파일이 변경되거나 위치가 변경됩니다. yarn berry에서는 단 하나의 패키지 파일만 추가/삭제되기 때문에 성능과 보안 관점에서 유리합니다.
왜 프로젝트에 Yarn berry를 도입하였는가?
-
github actions 를 이용하여 CI pipeline을 구축하였습니다.
-
github actions는 각 job이 서로 다른 인스턴스 위에서 실행됩니다.
-
초기화된 인스턴스에는 모듈이 설치되어 있지 않으므로 이를 설치하는 과정에서 꽤 많은 시간이 소요되고, 각각의 job에서 모듈을 설치하므로 시간이 많이 허비됩니다.
-
모듈 설치시간을 단축시키기 위하여 yarn berry를 도입하여 zero-install 전략을 채택하였습니다.
도입 결과
yarn classic FE CI 시간(A) | yarn berry FE CI 시간(B) | diff(A-B) |
---|
5m 37s | 3m 31s | 2m 06s(37.39%) 단축 |
도입 전
![yarn classic FE CI 시간 개요](https://velog.velcdn.com/images%2Fbigsaigon333%2Fpost%2F20c0e5bd-7a58-4e2c-a674-4d8b8d628f77%2Fclassic-CI-1.png)
![yarn classic FE CI 시간 상세](https://velog.velcdn.com/images%2Fbigsaigon333%2Fpost%2Fcfac2fa9-9dc7-409b-915e-43efb380d6bd%2Fclassic-CI-2.png)
도입 후
![yarn berry FE CI 시간 개요](https://velog.velcdn.com/images%2Fbigsaigon333%2Fpost%2Face4138f-ca40-4236-837c-a9363085381e%2Fberry-CI-1.png)
![yarn berry FE CI 시간 상세](https://velog.velcdn.com/images%2Fbigsaigon333%2Fpost%2F96cb6b7e-96c0-4e08-b0b6-f3537c9bbb48%2Fberry-CI-2.png)