[Node.js] npm 의존성 관리 원리

Falcon·2023년 4월 19일
5

javascript

목록 보기
22/28
post-thumbnail

Global option

Global module path

linux 환경에서 보통은 /usr/local/lib/node_modules 에 설치된다.

$ npm root -g

CLI 커맨드 생성

global 설치시 CLI 커맨드를 입력할 수 있게된다

$ npm install -g pm2
$ pm2 list # CLI 생성

Global vs Local 참조 우선순위

원래 nodejs의 모듈 참조 우선순위는 가장 가까운 node_modules (현재 디렉토리 -> 상위 -> 상위 -> ...-> Project Root)다.
그러나 bin 으로 등록되는 CLI 명령어는 우선순위가 다르다.

테스트를 위해 다음과 같이 프로젝트를 구성했다.

# local project 에 5.3.0 버전 설치
$ npm install pm2@5.3.0
# npm root (global) 에 5.1.2 버전 설치
$ npm install -g pm2@5.1.2

코드 레벨

// 프로젝트 local node_modules 우선참조
import pm2 from 'pm2'

CLI 명령어

pm2 CLI 명령어 실행결과

Local 설치 여부Global 설치 여부프로젝트 디렉토리상 실행 결과프로젝트 바깥 경로에서 실행 결과
OO5.3.05.3.0
XO5.1.25.1.2
OX5.3.0Not found : pm2

global 및 local 에 모두 설치시 global 우선.

local project 패키지를 우선 잡는 방법

위 실험에서 global 패키지가 local 패키지 보다 우선권을 갖는 것을 확인했다.
이는 환경변수 (ex. PATH) 에 먼저 잡히기 때문이다.

사실 global 버전은 하나로 관리되기 때문에 시시각각 변할 수 있어 다른 프로젝트에 영향을 미칠 수 있다. 따라서 local project package 기준으로 실행되게끔 보장하는게 필요한데, 이 때 쓸 수 있는 방법이 2가지다.

1. package.json 'scripts'

{
  "name": "devdependencies",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www",
    "ts": "ts-node -v"
  },
  "dependencies": {
    "uuid": "^9.0.0"
  },
  "devDependencies": {
    "ts-node": "^10.9.1"
  }
}

위와 같이 package.json 파일 내에서 scripts 를 등록하면 모두 로컬 패키지 기준으로 잡힌다.

# 전역 설치
$ npm install -g ts-node@8.1.1
# local 설치
$ npm install ts-node@10.9.1

$ ts-node -v #8.1.1 ( global 우선)
# npm script 혹은 npx 를 사용하면 local 프로젝트 기준으로 잡힌다.
$ npm run ts # 10.9.1 
$ npx ts-node -v #10.9.1

2. npx

global 설치시 명령어 실행 위치와 관계 없이 곧바로 사용이 가능하지만, 시스템 전체에서 강제로 글로벌에 설치된 버전을 사용해야한다는 단점이 존재한다.

이럴 때 npx 를 사용하면 다음과 같은 순서로 패키지를 찾아 CLI 를 실행한다.

  1. 현재 local 프로젝트 node_modules/bin 을 탐색, 존재시 실행
  2. 없으면 global node_modules 에서 bin 을 찾아 CLI 명령어를 실행
  3. global 에도 없으면 설치 여부 질문
Need to install the following packages:
  pm2@5.3.0
Ok to proceed? (y) y

3번 케이스를 통해 설치한 경우 현재 package.json 에 의존성이 추가되지 않는 것에 유의하라.

npx 로 패키지 설치시 npm cache 디렉토리에 설치된다.
즉, Local Project, Global 이 아니라 npm-cache 에 설치되므로 시간이 지나면 사라진다.

Windows 기준: C:\Users{YourUserName}\AppData\Local\npm-cache_npx\

{
  "dependencies": {
    "pm2": "^5.3.0"
  }
}

dependencies vs devDependencies

{
  // 실제 production 에 설치
    "dependencies": {
        "dotenv": "^6.2.0",
        "express": "^4.16.4",
        "mongodb": "^4.0.0",
        "node-fetch": "^2.6.1",
        "pino": "^5.11.1",
        "workerpool": "^6.1.5"
    },
  // 개발 및 테스트시에만 설치, npm scripts 에 사용 가능.
    "devDependencies": {
        "@typescript-eslint/eslint-plugin": "^4.21.0",
        "@typescript-eslint/parser": "^4.21.0",
        "eslint": "^7.32.0",
        "eslint-config-prettier": "^8.3.0",
        "eslint-plugin-prettier": "^3.3.1",
        "mocha": "^9.1.3",
        "ts-node": "^8.10.2",
        "typescript": "^3.9.9"
    }
}

흔히 많이 하는 착각이 npm install 하면 dependencies 만 설치되고 devDependencies 에 있는건 설치되지 않는 줄 아닌데 실제로는 node_modules 에 둘다 설치된다.

dependencies 설치

$ npm install uuid

덩그러니 uuid 패키지만 설치하면 다음과 같이 node_modules 에 설치된다.

devDependencies 는 실제로 node_modules 에 설치되는가?

설치 된다.

--dev 옵션으로 ts-node 설치해보자

$ npm install --dev ts-node

설치된 것은 물론이고 CLI 명령어도 인식된다.

# devDependencies 에 명시한 ts-node 가 node_modules 로 설치되고
# 현재 프로젝트에서 이 모듈을 사용한다.
$ ts-node -v
# 10.9.1

devDependencies 에 설치된 명령어가 어떻게 CLI 로 실행되는가?

자세히 보면 node_modules/bin 디렉토리에 ts-node 와 관련된 실행파일이 설치된 것을 확인할 수 있다.

실행파일 위치 확인이 필요할 때는 다음 2가지 명령어를 쓰면 된다.

# linux
$ which <command>

# windows
$ where <command> # CMD 인 경우
$ get-command <command> # powershell 인 경우 

특정 패키지 의존성 검사 방법

$ npm list -a <package-name>

예시

$ npm list -a uuid
+-- aws-sdk@2.940.0
| `-- uuid@3.3.2
+-- pm2@5.3.0
| `-- @pm2/io@5.0.0
|   +-- @opencensus/core@0.0.9
|   | `-- uuid@3.3.2 deduped
|   `-- @opencensus/propagation-b3@0.0.8
|     +-- @opencensus/core@0.0.8
|     | `-- uuid@3.3.2 deduped
|     `-- uuid@3.3.2 deduped
`-- swagger-stats@0.99.1
  +-- request@2.88.2
  | `-- uuid@3.3.2 deduped
  `-- uuid@8.3.2

Q1. npm install 시 버전이 어떻게 결정되는가?
A1. package.json 의 dependencies, devDependencies 에 의해 결정된다.

VersionCompatibilityVersion exampleCode status
MajorIncompatible2.0.0New API not backward
MinorCompatible1.2.0new features but backward
PatchCompatible1.0.1Bug fix

tilde(~): x.y.z 중 z (Patch version) 범위 내에서 버전 업데이트
caret(^): x.y.z 중 x (Major version) 이하 하위 호환성이 보장되는 범위 내에서 버전 업데이트

    "dependencies": {
      // 메이저 버전을 유지하면서 (하위 호환성을 유지하면서 패키지 설치 시점의 최신 모듈을 설치 
    	"aws-sdk": "^2.489.0", // 2.y.z 중 가장 최신 버전이 설치됨.
	}

2023-04-25 기준 aws-sdk 2.1360.0 이 설치된다.

Q2. 그럼 dependencies, devDependencies 에 없다면?
A2. 패키지가 package.json 에 전혀 명시되지 않았는데 설치되는 경우는 이미 package.json 에 존재하는 패키지가 다른 패키지에 대한 의존성이 있는 경우다.
이럴 때는 해당 패키지 내의 package.json에 따라 최신 버전으로 설치되고, 설치된 버전을 package-lock.json 에 기록한다.

Q2. uuid 모듈 버전이 3.3.2 도 있고 8.3.2 도 있다. package.json 에는 uuid 를 포함하지 않았다. 이럴 땐 어느 모듈로 설치되는가?

A2. 3.3.2 로 설치된다.
자세히 보면 3.3.2에 dedeuped 라는 문장이 옆에 붙어있다.
'중복 제거'라는 뜻으로 npm이 의존성 충돌이 발생하지 않는 대다수의 버전을 3.3.2로 인식하고 uuid@3.3.2 를 node_modules 경로에 설치하기로 결정한 것이다.
따라서 aws-sdk, pm2, swagger-stats/request@2.88.2 는 node_modules/uuid 경로의 패키지를 그대로 참조하게된다.
(항상 {project-root}/node_modules 에 설치되는 것은 아니다.)

Q3. 그럼, uuid 8.3.2 는 어디에 설치되나?

A3. 이때는 npm이 swagger-stats/node_modules/uuid 로 직접 설치한다.

반대로 swagger-stats 는 3.3.2가 아닌 8.3.2 가 직접 필요하다.
swagger-stats/package.json 에 의존성 정보중 일부다.

{
  "version": "0.99.1"
  "_requested": {
    "type": "version",
    "registry": true,
    "raw": "swagger-stats@0.99.1",
    "name": "swagger-stats",
    "escapedName": "swagger-stats",
    "rawSpec": "0.99.1",
    "saveSpec": null,
    "fetchSpec": "0.99.1"
  },
  "_requiredBy": [
    "/"
  ],
  "author": {
    "name": "https://github.com/sv2"
  },
  "bugs": {
    "url": "https://github.com/slanatech/swagger-stats/issues",
    "email": "sv2@slana.tech"
  },
  "dependencies": {
    "basic-auth": "^2.0.1",
    "cookies": "^0.8.0",
    "debug": "^4.3.1",
    "moment": "^2.29.1",
    "path-to-regexp": "^6.2.0",
    "qs": "^6.10.1",
    "request": "^2.88.2",
    "send": "^0.17.1",
    "uuid": "^8.3.2" // ✅ 8.3.2 이상 버전이 필요하다.
  },
  "description": "API Telemetry and APM. Trace API calls and Monitor API performance, health and usage statistics in Node.js Microservices, based on express routes and Swagger (Open API) specification",
  "devDependencies": {
    "@hapi/hapi": "^20.1.2",
    "@hapi/inert": "^6.0.3",
    "artillery": "^1.6.2",
    "body-parser": "^1.18.2",
    "chai": "^4.1.2",
    "chokidar": "^3.5.1",
    "concurrently": "^6.0.0",
    "coveralls": "^3.1.0",
    "cross-env": "^7.0.3",
    "cuid": "^2.1.8",
    "express": "^4.17.1",
    "fastify": "^3.14.1",
    "fastify-express": "^0.3.2",
    "istanbul": "^0.4.5",
    "mocha": "^8.3.2",
    "ncp": "^2.0.0",
    "nyc": "^15.1.0",
    "prom-client": "^13.1.0",
    "q": "^1.5.1",
    "restify": "^8.5.1",
    "serve-favicon": "^2.4.5",
    "serve-static": "^1.13.1",
    "should": "^13.2.3",
    "supertest": "^6.1.3",
    "swagger-parser": "^10.0.2",
    "swagger-stats-ux": "^0.95.28"
  },
}

대부분의 패키지가 3.3.2 버전을 참조하므로 {ProjectRoot}/node_modules/uuid 에 설치해버렸다.
반면에swagger-stats 는 8.3.2 버전이 필요하다.
이럴 때는 {ProjectRoot}/node_modules/swagger-stats/node_modules 패키지 자체 내에 node_modules 를 설치하여 관리한다.
이로써 한 패키지에 대해 여러 버전으로 runtime 실행이 가능한 것이다.

Q4. nodejs / npm 버전이 package 의존에 영향을 미치나?
A4. 미친다. npm 버전이 바뀌면 package-lock.json 의 lockfileVersion 1,2,3 도 바뀔 수 있다.

package-lock.json 은 다음 3가지가 모두 일치해야만 동일하게 유지된다.
1. npm version
2. package.json
3. npm install 시점

npm 버전은 node.js 버전 패치에 따라 자동으로 바뀌므로 유의해야한다.

Q5. npm audit fix 하면 어떻게 버전이 바뀌는가?
A5. semmentic versioning 대로 업데이트된다.
단, npm audit fix --force 하면 취약점이 존재하는 패키지의 메이저 버전도 바꿀 수 있다.

# package.json 에 sementic versioning 정책대로 업데이
$ npm audit fix

보안 이슈 있는 패키지 버전을 compatible 유지한체 업데이트함.

# 메이저 버전도 업데이트 
$ npm audit fix --force

Q5. 프로젝트 전체에 걸쳐 특정 패키지 버전을 고정시키고 싶다. 즉, {ProjectRoot}/node_modules/{package} 내부에 설치되는 특정 패키지의 버전을 전역으로 고정시키고 싶다. 어떻게 해야하는가?

A5. overrides 속성을 사용하면된다.
⚠️ 단, nodejs 16.x - npm 8.x 버전부터 사용 가능하다.

package.json 예시

uuid 를 프로젝트 전체에서 9.0.0 버전으로 고정한다.

{
  "overrides": {
    "uuid": "9.0.0"
  },
  "engines": {
    "node": ">=16.18.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.17",
  },
  "scripts": {
    "test": "jest",
    "compile": "tsc -p ."
  },
  "dependencies": {
    "express": "^4.18.2",
    "pm2": "^5.3.0",
    "workerpool": "^6.3.1"
  }
}
$ npm list -a uuid
`-- pm2@5.3.0
  `-- @pm2/io@5.0.0
    +-- @opencensus/core@0.0.9
    | `-- uuid@9.0.0 overridden
    `-- @opencensus/propagation-b3@0.0.8
      +-- @opencensus/core@0.0.8
      | `-- uuid@9.0.0 deduped
      `-- uuid@9.0.0 deduped

Q6. 본 패키지를 nodejs 특정 버전 이상에서만 실행시키고 싶다. 어떻게 해야하나?
A6. node 를 실행시키는 것 자체를 막을 수는 없는 대신, npm install 을 불가능하게 제한할 수는 있다.
package.json 에 아래와 같은 옵션을 두면된다.

  "engines": {
    "node": ">=16.18.0"
  },

이때 npm 의 경우 .npmrc 를 다음 옵션과 함께 넣어줘야한다.

engine-strict=true

반면에 yarn 은 위 옵션만으로 npm install 을 제한한다.
node 버전을 14.21.3 으로 설정해놓고 npm install 을 하면 다음과 같은 에러가 발생한다.

npm ERR! code ENOTSUP
npm ERR! notsup Unsupported engine for @: wanted: {"node":">=16.18.0"} (current: {"node":"14.21.3","npm":"6.14.18"})
npm ERR! notsup Not compatible with your version of node/npm: @
npm ERR! notsup Not compatible with your version of node/npm: @
npm ERR! notsup Required: {"node":">=16.18.0"}
npm ERR! notsup Actual:   {"npm":"6.14.18","node":"14.21.3"}

결론

모듈 참조 우선순위

모듈 참조는 현재 디렉토리 -> Project Root 까지 탐색.
bin으로 등록되는 CLI 명령어 모듈은 Global -> Project Root 탐색
현재 디렉토리 기준으로 CLI 명령어 버전관리 또는 일회성 CLI 커맨드 (ex. eslint, create react-app) 사용시 npx 사용.

package 버전 및 설치 위치 결정 방식

  • package.json 에 tilde(~) 또는 caret(^) 있는 경우 해당 범위 내의 최신 버전을 설치 (package-lock.json 업데이트 발생)
  • package.json 에 없는 모듈의 경우 package-lock.json 에 의해 버전이 결정되고 다음 위치에 설치됨.
    - 충돌없이 가장 많이 사용되는 공통 버전 -> {Project-Root}/node_modules 에 설치.
    - 나머지 버전-> 해당 패키지를 참조하는 패키지 내부 {other-package}/node_modules 에 설치.

Reference

profile
I'm still hungry

0개의 댓글