모듈을 require 할 때의 과정

require('sample')로 모듈을 로드할 때,

sample.js 라는 파일로 존재하는 모듈을 로드할 수 있고, sample 이라는 디렉토리 안의 모듈을 로드할 수 있다.

 

디렉토리 이름인 경우 오른쪽 절차에 따라 로드한다.

 

서드파티 모듈을 로드할 때는,

모듈의 이름이 디렉토리인 경우, 그 안에 package.json이라는 포함하고 있는 지 확인한다.

그렇니까 서드파티 모듈은 package.json 이라는 파일을 가지는 디렉토리인 것이다.

 

 

서드파티 모듈을 로드할 때의 경로

package.json 파일을 가진 디렉토리 형태로 존재하기 때문이다. 

그렇기 때문에 서드파티 모듈의 이름이 디렉토리 이름과 같았던 것이다.

 

정리하자면

패키지 안에 package.json 파일이 있을 때, package.json 파일의 내용 중
(1) main 필드가 존재하면 거기에 적힌 파일을 로드하고

(2) main 필드가 존재하지 않으면 index.js 파일을 로드한다.

 

(1)의 경우 뿐만 아니라 (2)의 경우에 해당하는 패키지들도 종종 존재하기 때문에 잘 기억해두어야 한다! 


package.json

  1. package.json이라는 파일을 가진 디렉토리가 패키지이다.
  2. 하나의 서드파티 모듈은 하나의 패키지이다. 즉, 서드파티 모듈 = 패키지
  3. npm = node package manager

그럼 도대체 package.json 은 무엇일까?

해당 패키지에 관한 정보를 가지고 있는 파일이다.

 

author : 패키지를 만든 사람에 관한 정보

contributors : 패키지를 만들 때 기여한 사람에 대한 정보

dependencies : 이 패키지를 사용하기 위해 필요한 다른 패키지들에 관한 정보

 

engines : 이 패키지를 실행하기 위한 노드의 버전 정보

 

main : 이 패키지를 로드했을 때, 실제로 로드되는 파일의 이름이 적혀있는 필드이다.

 

예를 들어, A라는 패키지가 있고, A 패키지의 package.json 파일의 내용 중, main 필드에 start.js라는 값이 적혀있다고 하자.

  1. 그럼 해당 프로젝트에 있는 다른 어떤 자바스크립트 파일 안에서
  2. require('A') 코드는 결국 start.js 파일을 로드한다는 뜻이고,
  3. 이 start.js 파일 내의 코드에서 exports, module.exports 등으로 외부에 공개한 객체를 가져오게 되는 것이다.

대부분의 패키지가 이런 방식으로 사용되기 때문에 보통 package.json 파일에는 main 필드가 존재한다.

만약 main 필드가 없다면, 작업 디렉토리 안에서 index.js라는 파일을 찾아서 로드한다.


패키지의 버전

dependencies 필드에 있는 각 패키지 이름 옆의 버전은 Semantic Version이라고 하는데, Semantic Version을 우리말로 해석하면 '의미론적 버전' 정도로 해석할 수 있다.

 

Semantic Version는 위 그림에서 보이는 것처럼 1.3.7, 6.7.0 등과 같이 총 세 개의 숫자로 이루어진 버전이다. 이때 이것을

이 이미지처럼 쪼개봤을 때,

X를 메이저 버전(major version) Y를 마이너 버전(minor version) Z를 패치 버전(patch version)이라고 한다. 

Semantic 버전에는 규칙이 있다. 그전에 API를 알아야한다.

 

API란 Application Programming Interface의 약자로 '외부에서 사용할 수 있도록 공개된 함수'를 의미한다.  우리가 코드 내에서 require 함수를 써서 어떤 패키지를 로드하는 이유는 뭘까? 그 패키지가 공개하는 함수 등을 사용하기 위해서이다.  이렇게 외부에서 사용할 수 있도록 공개된 함수 등을 모두 API라고 하는 것이다. Semantic Version에서는 이 API의 변화를 기준으로 버전을 업데이트해야 한다.

 

첫 번째로 가장 오른쪽의 패치 버전은, API에 변화를 주지 않는 범위 내에서의 변화가 이루어진 경우에 업데이트 한다. 이런 경우는 어떤 것들이 있을까? 겉으로 공개된 API는 바뀌지 않았지만, 코드에 존재하던 버그를 해결하거나, 알고리즘을 바꿔서 그 효율성을 향상시킨 경우 등이 해당한다. 이럴 때는 예를 들어, 버전을 2.3.1에서 2.3.2로 올릴 수 있는 것이다. 2.3.1에서 바로 2.4.0이나 3.0.0으로 업데이트하면 안 되는 것이다.

 

두 번째로 가운데에 있는 마이너 버전은, 이전 버전의 API와 호환되는(backward-compatible) API 상의 변화가 발생했을 때 업데이트한다. 예를 들어, 새로운 API를 추가한 경우를 생각해보자. 그러니까 2.3.1 버전에서 새로운 API를 추가하면 2.4.0으로 버전을 올리면 된다. 그럼 기존의 2.3.1 버전의 패키지를 믿고 사용했던 다른 곳에서 이 패키지의 2.4.0 버전을 사용해도 괜찮은 걸까? 괜찮다. 왜냐하면 API 상의 변화가 생기긴 했지만 이미 존재했던 API들은 건드리지 않는 범위의 변화(단순 API 추가)가 발생한 것이기 때문이다.

 

마지막으로 가장 왼쪽의 메이저 버전은, 이전 버전의 API와 호환되지 않는(not backward-compatible) API 상의 변화가 발생했을 때 업데이트 한다. 기존의 API를 아예 삭제했거나 그 이름을 바꾸는 등의 변화가 이것에 해당하는데, 이럴 때는 원래 2.3.1 버전이었다면 3.0.0으로 버전을 올려줘야 한다. 만약 자신이 사용하던 패키지의 메이저 버전이 업데이트되었다면 그리 좋은 소식은 아닐 수도 있다. 왜냐하면 그 패키지의 최신 버전을 사용하고 싶다면, 원래 사용하던 이전 버전 패키지의 API에서 어떤 부분들이 바뀐 건지를 체크하고, 코드를 재수정해야 할 가능성이 높기 때문이다.

 

그리고 메이저 버전 업데이트는 패키지를 만드는 사람 입장에서도 너무 자주 해서는 안 되는 작업이기도 하다. 특히, 인기가 많은 패키지일수록 해당 패키지에 의존해서 만들어진 것들이 많을 텐데 만약 메이저 버전 업데이트가 자주 발생하면 이것에 대해 많은 사람의 수고가 필요하기 때문이다. 


내가 만든 모듈을 패키지로 만들기

$ npm init

현재 디렉토리를 하나의 패키지로 만들 때 쓰는 명령어이다.

 

  • package name : 패키지 이름 입력
  • version : 버전 정보 입력 ex) (1.0.0)
  • description : 패키지에 대한 설명 입력
  • entry point : 다른 패키지에서 내가 만든 패키지를 로드할 때, 실제로 로드될 파일 ex) (main.js)
  • test command : 잘 동작하는지 확인하는 명령어
  • git repository : 깃으로 작업한다면 그에 맞는 레포지토리
  • keywords : 검색했을 때 나오는 키워드 
  • author : 패키지 작성자 
  • license

 

다 입력하면 package.json 에 대한 정보가 나온다.

 

여기서 신기한 점은 자동으로 들어가 있는 dependencies 필드인데, npm init을 하기 전에 미리 설치한 모듈이 있다면 자동으로 설치된다.

npm이 나의 디렉토리를 분석해서 자동으로 채워주기 때문이다.

 

마지막으로 yes를 입력하면 package.json 파일이 생긴다.


패키지를 공유할 때, node_modules 디렉토리는 공유하지 않는다.

하나의 패키지는 여러 명의 개발자가 협력해서 만드는 게 일반적이다.

그럼 이때 협력하는 개발자들끼리는 패키지를 어떻게 공유할까?

 

패키지는 package.json 파일이 있는 디렉토리니까 이 디렉토리를 서로 주고받으면 되는데, 이때 중요한 점이 하나 있다. 패키지를 공유할 때, 패키지 안에 있는 node_modules 디렉토리는 보통 공유하지 않는다는 점이다.

 

node_modules 디렉토리는 현재 작업 중인 패키지에서 이런저런 패키지들을 설치하면 그것들이 설치되는 곳이었다. 그런데 이렇게 중요한 node_modules 디렉토리를 왜 공유하지 않는 걸까?

 

왜냐하면 보통 패키지를 몇 개 정도만 설치해도 node_modules 디렉토리 내부의 용량은 매우 커지게 되고, 이것들을 매번 공유하는 것은 비효율적(용량 문제)이기 때문이다. 하지만 그렇다고 해서 현재 패키지가 의존하는 다른 패키지들이 없으면, 현재 패키지가 정상적으로 실행될 수 없을텐데 대책이 있는 걸까?

 

바로 이때 package.json 파일이 필요하다.

패키지를 공유할 때는 그 안의 package.json 파일만 제대로 공유하면 된다. 그리고 공유받은 측은, 해당 패키지 안에서

$ npm install

이라고 쓰고 실행하면, npm이 package.json 파일의 dependencies 필드에 적힌 의존 패키지들의 이름과 Semantic Version / Version Range Syntax를 보고 알맞은 패키지들을 자동으로 설치해준다.

 

즉, 패키지를 공유할 때 무거운 용량의 node_modules 디렉토리는 굳이 직접 공유하지 않고, 패키지를 공유받는 사람이 npm install 명령어를 실행해서 공유받은 package.json 파일을 기반으로 직접 생성하는 것이다.


package.json과 package-lock.json의 차이

그러나 가끔 npm install로 모듈을 설치하는, 이 방식이 문제가 될 때가 있다. 그건 바로 개발자 A가 갖고 있던 패키지를 받아서 개발자 B가 npm install 명령어로 그 안의 node_modules 디렉토리를 재현해도, 그 내부의 패키지들이 동일하지 않을 때가 있기 때문이다.

 

이런 일이 발생하는 가장 주된 원인은 우리가 배운 Version Range Syntax 때문이다.

 

package.json 파일의 dependencies 필드를 보면 버전 부분에 3.5.2 이런 식으로 딱 Semantic Version만 적혀있는 부분도 있지만  Tilde Range(~3.5.2), Caret Range(^3.5.2) 같은 Version Range Syntax가 적혀있는 경우도 많다. 그리고 이런 경우에는 해당 패키지의 특정 버전 뿐만 아니라 일정한 버전 범위 내에 있는 패키지면 설치가 가능하기 때문에 문제가 되는 겁니다.

 

예를 들어,

  • 개발자 A가 sample이라는 패키지에 관한 작업을 할 때,
  • 그것이 의존하는 helper라는 패키지의 Version Range가 ^3.5.2였다고 생각해보자.

 

그럼 3.5.2≤ package <4.0.0 버전 안의 패키지들은 모두 설치가 가능할 것이다.

 

A가 sample 패키지를 만들 때는 helper 패키지의 최신 버전이 3.5.4였다고 해보자. 그리고 시간이 지나 helper 패키지의 3.6.0 버전이 나왔다고 생각해보자. 이 시점에 만약 개발자 B가 이 패키지를 공유받는다면 어떻게 될까? 위에서 말한 대로 node_modules 디렉토리는 공유하지 않고 package.json 파일만 공유하기 때문에, 이제 개발자 B의 경우에는 npm install을 실행했을 때 3.6.0 버전의 helper 패키지가 설치될 것이다.

 

분명, 둘 다 sample 패키지를 갖고 작업을 하고 있는데

  • 개발자 A는 3.5.4 버전의 helper 패키지,
  • 개발자 B는 3.6.0 버전의 helper 패키지

 

를 갖고 작업하게 되는 것이다.

 

일반적인 경우에는 이러한 것들이 별 문제가 없을 수도 있다. 하지만

  • sample 패키지를 공동 개발하는 중이라 모든 개발자가 똑같은 환경을 보장받아야하는 경우
  • helper 패키지의 새 버전에 버그 등은 없는지 점검한 후에 써야하는 경우

 

등에는 공유하는 측과 공유받는 측에서 이렇게 dependency의 차이가 발생해서는 안 된다.

 

그럼 이 문제를 어떻게 해결할 수 있을까? 그렇다고 node_modules 디렉토리를 통째로 공유하는 건 용량 때문에 너무 비효율적인데 말이다.

 

정답은 바로 package-lock.json 파일에 있다. 

 

  • package-lock.json 파일은 맨 처음 패키지를 설치했을 때(위에서는 '서드파티 모듈'이라고 불렀다.) 생겼던 파일이고
  • package.json 파일은 npm init 명령어를 사용해서 디렉토리를 하나의 패키지로 만들었을 때 생긴 파일이다.

 

package-lock.json 파일에는 dependencies 필드에, 현재 패키지 안에 어떤 패키지들이 설치되어 있는지 그 정보가 담겨있다고 했고,
package.json 파일도 마찬가지로 dependencies 필드가 있고, 여기에는 어떤 패키지들이 설치되어야 하는지에 대한 정보가 있다고 했다.

 

 

방금 위의 설명에서 둘 간의 미묘한 차이를 눈치챘는가?

 

package-lock.json에는 현재 패키지 안에 '실제로 설치된' 패키지들의 정확한 버전들이 기록되어 있다.

반면에 package.json에는 dependencies 필드에 보이는 패키지의 이름 옆에는 Version Range Syntax가 존재하는 경우도 있다는 것을 알 수 있다. 즉, 이 dependencies 필드가 의미하는 것은 특정 패키지의 특정 버전이 아니라(특정 버전이 적혀있는 경우도 물론 있다) Version Range Syntax를 만족하는 버전의 패키지라면 언제든지 업데이트된 버전의 패키지가 설치되어도 괜찮다는 뜻이다.

 

그러니까

package-lock.json 파일의 dependencies 필드에는 '현재 패키지에 실제로 설치되어 있는 다른 패키지들의 버전'이 적혀있는 것이고,

package.json 파일의 dependencies 필드에는 '현재 패키지가 동작하기 위해 필요한 다른 패키지들의 버전 범위'가 적혀있는 것이다.

 

그리고 바로 이 package-lock.json 파일이 아까 말한 'package.json 기반의 패키지 공유 방식'이 발생시킬 수 있는 문제에 대한 해결책이다. 패키지를 공유할 때, 이 package-lock.json 파일도 package.json 파일과 함께 공유하면

$ npm install

을 실행했을 때 npm은 package-lock.json 파일의 dependencies를 보고, 특정 버전의 패키지들을 정확히 동일하게 설치한다.

그럼 어느 상황에서든 해당 패키지를 공유받는 사람이나 공유해준 사람은 동일한 버전의 dependency들을 설치하게 된다. 즉, 동일한 node_modules 디렉토리를 갖게 되는 것이다.

 

그러니까 만약 패키지를 공유할 때 정확히 똑같은 패키지를 상대방도 가져야 할 때는 반드시 이 package-lock.json 파일도 package.json함께 공유해줘야 한다. 그 정도까지는 아니고 공유받는 측에서 패키지가 동작만 잘 해도 되는 경우라면 package.json 파일만 공유해줘도 되고.

 

정리하자면

  1. 패키지를 공유할 때는 보통 그 안의 node_modules 디렉토리를 제외하고 공유한다.
  2. 이때 패키지 안의 package.json 파일 내용 중 dependencies 필드의 정보가
  3. 공유받는 측에서 node_modules 디렉토리를 재생성(npm install)하는 데 사용되며
  4. 이때 공유해준 사람과 공유받은 사람 간에 node_modules 디렉토리 내부의 차이가 발생하지 않도록 방지하려면
  5. package-lock.json 파일도 package.json 파일과 함께 공유해줘야 한다.

package-lock.json 만 공유하면 안되나?

package.json package-lock.json 파일을 두 가지 다 공유하는 것이 안전하다.

 

실제로,  package-lock.json이 있다고 해도, npm installl을 통해 갱신되는 경우가 있다.

즉, 같은 개발환경을 구축할때도, npm installl을 사용할 수 없을 가능성이 있다. npm의 버전에 따라 미묘한 구동의 차이나, 다양한 유스케이스, 인위적인 미스가 있기 때문에, 같은 개발환경 구축이 어렵다는 가능성이 있다.

 

역으로, 누군가가 package.json만 수정했다면, package-lock.json을 commit이나 push를 하지 않아, 충돌해결이 어려웠던 경우도 쉽게 나타난다.

  1. npm install  package.json을 설치하고 그럼에도 버전에 따른 에러가 발생할 경우,
  2. npm ci 를 이용해 package.jsonpackage-lock.json을 비교하며 완전히 같은 환경을 구축해준다.

sudo npm install -g nodemon

 

 

nodemon 패키지는 하나의 실행파일 처럼 실행할 수 있다.

nodemon 패키지는 수정된 코드를 자동으로 재실행 해준다.

 

패키지를 설치하는 방법에는 크게 두 가지가 있다.

 

첫 번째는 우리가 이때까지 일반적으로 했던 것처럼 -g 옵션을 주지 않고 그냥 설치하는 방법이다. 이것을 local mode 설치라고 한다. 이 경우에는 이전에 배운 것처럼 패키지가 node_modules 디렉토리에 설치된다.

 

두 번째는 이전 영상에서 했던 것처럼 -g 옵션을 주고 설치하는 방법이다. 이것을 global mode 설치(전역 설치)라고 한다. 그런데 이 경우에는 패키지가 node_modules 디렉토리가 아닌 다른 곳에 설치된다. 

{prefix}/lib/node_modules

'🚦 Server > Node.js' 카테고리의 다른 글

ORM으로 DB 작업하기  (0) 2021.10.31
express 시작하기  (0) 2021.10.28
Node js 정리  (0) 2021.10.19
Node - 화살표 함수  (0) 2021.08.10
Node - const, let 는 var를 대체한다.  (0) 2021.08.10
복사했습니다!