[Node.js] npm (Node package manager) : npm에는 왜 package.json 과 package-lock.json이 존재할까?

- 의존성패키지 : 해당 프로젝트를 실행하는 데 꼭 필요한 라이브러리

ex) express 라이브러리로 서버를 동작하게 만든 node프로젝트는 express에 의존성이 있다고 할 수 있다.

 

- 패키지 : Node.js에서의 패키지는 package.json으로 정의한 파일 또는 디렉터리를 의미한다. 패키지에는 package.json이 포함된다. 즉 package.json으로 정의한 코드 뭉치가 패키지이다.

 

- 모듈 : npm install명령으로 설치한 패키지들. 이는 node_modules 에 저장된다. 즉 모든 패키지는 모듈이다. require(Common JS)나 import(ESmodule)문으로 읽을 수 있다. 

 

- package.json은 수동으로 만들 수도 있으나 npm init 명령어로 자동으로 만드는 것을 추천

 


npm

- npm init : 도입 

- npm install, npm i : 설치 (명령어를 이용해 현재 프로젝트의 종속성을 설치한다.) 

 

install 옵션 

- npm install -d : dev dependencies : 개발환경, 실제 개발시에만 사용하는 패키지를 정의 , 다른곳에서 해당 패키지 설치시에는 포함되지 않는다. 

- npm install -g : 프로젝트 디렉터리가 아닌 node가 설치되어있는 루트의 node_modules에 의존성 패키지 설치 

 

 

패키지 업데이트

npm update, npm up, npm upgrade [패키지명]의 명령어를 통해 업데이트 할 수 있다.

이 때 [패키지명]에 아무것도 없으면 package.json에 정의한 의존성 패키지 모두를 업데이트 한다.

 

package.json에 의존성 패키지 버전을 적을 때 ^ 또는 ~ 기호를 사용한다. 

 

^ (caret):

  • 발음: 캐럿
  • 예시: "dependencies": { "package-name": "^1.2.3" }
  • 의미: 주 버전 번호가 변경되지 않는 한, 이 의존성은 최신 버전으로 업데이트 될 수 있다. 예를 들어, "^1.2.3" 버전은 1.x.x 범위 내의 모든 버전을 포함하며, 이 중 가장 최신 버전이 사용.

~ (tilde):

  • 발음: 틸드
  • 예시: "dependencies": { "package-name": "~1.2.3" }
  • 의미: 마이너 버전 번호가 변경되지 않는 한, 이 의존성은 최신 버전으로 업데이트될 수 있다. 예를 들어, "~1.2.3" 버전은 1.2.x 범위 내의 모든 버전을 포함하며, 이 중 가장 최신 버전이 사용.

 

요약하자면 ^는 주 버전까지의 업데이트를 허용하고, ~는 마이너 버전까지의 업데이트를 허용한다.

^는 더 넓은 범위의 버전 업데이트를 허용하며, ~는 보다 제한적인 범위의 업데이트를 허용한다. 

 

npm의 문제점 : require()함수

require() 함수를 사용할 때는 단순히 현재 디렉터리의 [node.modules]만 읽는 것은 아니다. module.paths에 있는 경로를 따라서 모듈을 찾는다.

라이브러리를 찾기 위해 순회하는 디렉토리의 목록을 확인하려고 할 때, Node.js에서 제공하는 require.resolve.paths() 함수를 사용할 수 있다. 이 함수는 npm이 검색하는 디렉토리의 목록을 반환한다.

sample-package디렉터리를 만들고 node 명령을 실행해 module.paths를 실행한 결과이다.

수많은 node_modules가 검색되지만, 사실 이중에는 만들지 않은 디렉토리도 있다.

(아예 생성도 되지 않았는데 패키지매 니저는 있다고 가정하고 설정한 경로)

 

실제 존재하지도 않은 경로를 계속 타고 거슬러 올라가면서 node_modules가 있는지 검사한다. 상위 디렉터리에 있는 패키지를 계속 타고 올라가면서 node_modules를 확인하고, 굉장히 많은 I/O를 실행한다. 이것이 require()함수가 무거워지는 원인이 된다.

 

 

스크립트 기능

npm은 명령어를 지정해 실행하는 기능을 제공한다.

스크립트 기능은 앱 시작, 중지, 빌드, 배포, 테스트 등의 명령어를 터미널에 매번 입력하지 않고 package.json에 정의함으로써 npm run 뒤에 붙여 조금 더 간편하게 명령어를 실행하는 기능.(start, stop, test, restart는 run 없이 바로 실행할 수 있다) 

보통 스크립트는 node_modules 디렉터리 아래에 설치된 패키지에 있기 때문에 경로를 지정해야 하지만, npx를 이용하면 경로를 지정하지 않고 사용할 수 있다. 

 

* 실행 전과 후에 실행될 스크립트를 지정할 수도 있다. 명령어 앞에 pre 혹은 post를 붙이면 된다.

 

ex) package.json 

{
	"name": "test-scripts",

	"version": "1.0.0"
	"scripts": {
		"hello": "echo 'hello Node.js'"
		"test": "echo 'test Node-js'" // npm test로 실행
		"stop": "echo 'stop Node.js'",// npm Stop으로 실행
		"start": "echo 'start Node.js'", // npm start로 실행
		"restart": "echo 'restart Node.js'" // npm restart로 실행	
	}
}

 

각각 실행 명령어를 입력하면

 

test Node.js

stop Node.js

start Node.js

restart Node.js 

 

가 터미널에 출력된다. 

 

 

 

"scripts": {
    "hello": "echo 'hello Node.js '", // npm run hello 로 실행
    "prehello": "echo 'PRE Hello'",
    "posthello": "echo 'POST Hello'"
  }

 

만약 스크립트에 위와 같이 쓰여있다면 

터미널에는 npm run hello 만 입력해도 prehello -> hello -> posthello 순서로 실행되어 

 

PRE Hello

hello Node.js

POST Hello 

 

가 출력될 것이다.

 

 

Npx (Node Package eXecute)

패키지 실행자 : 패키지들을 실행할 때는 node_modules/.bin/{패키지명} 경로로 명령어를 실행해야 하지만

npx를 사용하면 npx {패키지명} 처럼 경로를 생략해 실행할 수 있다.

 

ex) JS코드를 자동으로 포맷팅하는 prettier의 package.json 에는 아래와 같은 설정이 있다

이때 bin에 있는 설정이 npx에서 실행하는 명령어 파일이다. 

터미널에서 npx로 prettier를 실행할 때는 

npx prettier index.js  #1
npx prettier -w index.js #2

로 실행해주면 되는데, 

 

#1 : node_modules/prettier/bin-prettier.js를 실행하고 출력한다. 이 때 포매팅이 index.js 에 반영이 되지는 않는다.

#2 : -w옵션을 추가해 실행하면 prettier를 실행해 포매팅된 결과를 index.js에 반영한다. 

 

이렇게 사용할 수 있다. 

 

 

패키지 잠금을 위한 package-lock.json 

package.json에는 고정된 버전이 아닌 버전 범위를 설정할 수 있다. 따라서 패키지를 설치하는 시점에 따라 다른 버전이 설치될 수 있다. 또한 package.json을 변경하는 모든 명령(npm install, npm update, npm uninstall)이 실행되어 변경되면, package-lock.json도 변경될 수 있다. 하지만 package-lock.json은 정확한 버전을 기록한다. 요약하자면 차이는 이렇다

 

  1. package.json:
    • package.json :  버전 범위가 기록 
    • 예를 들어 "dependencies": { "package-name": "^1.2.3" }와 같이 기록되고, 이 경우 ^1.2.3은 1.2.x 범위 내의 모든 버전을 포함하고, 가장 최신 버전을 사용한다.
  2. package-lock.json:
    • package-lock.json : 실제 패키지의 정확한 버전과 해당 패키지가 의존하는 다른 패키지의 버전을 기록
      (의존성 트리의 정확한 상태를 기록하므로, package.json에 범위가 아닌 특정 버전이 기록된다)
    • 예를 들어, "packages": { "package-name": { "version": "1.2.3" } }와 같이 기록된다. 

 

이렇게 package-lock.json에 올바르게 동작하는 버전 정보를 저장해두고, 설치시package.json이 아닌 package-lock.json을 확인하고 설치한다. 이를 패키지 잠금 이라고 한다.

 

 

 

npm 의 문제점2 : 유령 의존성

npm은 프로젝트에 필요한 모든 의존성을 나열하고, 그중 어떤 것이 트리 구조의 상위에 필요한지, 어떤 버전이 어디에 필요한지를 식별한다. 이중 중복된 의존성 패키지는 위로 끌어올려 의존성 패키지 내부의 node_modules에 두지 않고, 상위에 위치한 패키지 바로 하단에 두어 사용한다. 단, 같은 라이브러리의 다른 버전을 필요로 하는 경우 좀 더 상위 라이브러리에 의존해 있는 버전이 위로 올려지고, 다른 버전은 의존하고 있는 라이브러리 하단에 위치하게 된다. 

 

이렇게 라이브러리들이 끌어올려지면, package.json에 정의하지 않아도, 같은 층에 위치하게 된다는 이유로 서로 사용할 수 있게 된다. 이를 유령 의존성이라고 한다. 

 

이 방식은 직관적이지 않으며, 정의하지 않은 것들도 사용할 수 있게 한다. 따라서 Node.js 개발 시에는 꼭 필요한 패키지가 아니라면 설치하지 않는 것이 좋다.


https://toss.tech/article/node-modules-and-yarn-berry

 

node_modules로부터 우리를 구원해 줄 Yarn Berry

토스 프론트엔드 레포지토리 대부분에서 사용하고 있는 패키지 매니저 Yarn Berry. 채택하게 된 배경과 사용하면서 좋았던 점을 공유합니다.

toss.tech