Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

npm v3에서 달라진 점

현재 npm은 npm v2와 npm v3 두 가지 버전이 사용되고 있다. npm은 Node.js를 설치할 때 포함되어 있는데 LTS 버전인 v4.x를 설치하면 npm v2가 포함되어 있고 최신 버전인 v6.x를 설치하면 npm v3가 포함되어 있다(Node.s v5 포함). 물론 Node.js 버전과 상관없이 npm의 버전을 올릴 수 있다.(npm install -g npm)

npm v2는 v1에서 발전하면서 올라온 버전이지만 v3는 기존의 문제점을 해결하려고 대부분 새로 작성되었다. 처음 npm v3가 나왔을 때 오랫동안 베타버전으로 관리되기도 했고 레거시 코드에서 문제를 굳이 일으키지 않으려고 npm v2를 그대로 사용하고 있었다. 사정상 Node.js LTS 버전으로 올리지 못하고 있다가 최근에 LTS 버전으로 바꾼 뒤 개인 작업은 모두 Node.js v6.x로 올라오면서 npm도 v3로 바꾸었다. 개인 사이드 프로젝트에서 큰 문제는 아직 없었고 v3의 대략적인 차이점을 알고 있기는 했지만, 자세히 알고 있는 것은 아니었다. npm v3가 나온지는 꽤 시간이 지났지만 혹시 생길 문제점도 예상하기 위해 차이점을 정리해 보게 되었다.(의존성이 꼬이는 문제는 항상 너무 피곤한 문제라서...)

npm 3

npm 3는 2015년 6월에 베타로 릴리즈되었고 2015년 9월에 안정 버전이 되었다. 여러 가지 내부 구조가 바뀌기도 했고 속도가 너무 느리다는 문제 등의 이슈도 계속 제기되었지만, 지금은 대부분 해결된 상태이다. 내가 겪지 못한 예외상황이 당연히 더 있겠지만 npm v2를 쓰고 있는 상황에서 npm v3로 올려도 큰 문제는 없을 것이라고 본다.

npm 3의 의존성 처리

npm 3에서 가장 큰 변경사항 중 하나가 의존성 처리방법의 변경이다. npm 2에서는 각 모듈 하위에 의존성 모듈을 설치한다. npm의 의존성 모듈은 node_modules 디렉터리 아래 설치되는데 프로젝트가 ab 모듈에 의존성을 가지고 있는데 각 모듈이 l, m 모듈에 의존성을 가지고 있다면 다음과 같이 설치된다.

node_modules/
├── a/
│   └── node_modules/
│       └── l/
└── b/
    └── node_modules/
        └── m/

이는 지금까지는 꽤 좋은 구조였다. 예를 들어 ab가 같은 l 모듈을 사용한다고 하는데 둘의 버전이 다른 경우 다음과 같이 사용하는 버전이 각각 모듈의 하위 디렉터리에 설치가 된다.

node_modules/
├── a/
│   └── node_modules/
│       └── l/   # v1.0.0
└── b/
    └── node_modules/
        └── l/  # v2.0.0

이 구조 덕분에 프로젝트에서 사용하는 모듈이 가진 의존성 간의 충돌은 신경 쓸 필요가 없다. 위는 설명을 위한 단순한 구조로 실제로 사용하는 모듈은 가진 의존성이 많으므로 이러한 계층 구조는 아주 복잡해 지지만 그 내의 다양한 버전과 모듈에서도 충돌 없이 잘 동작한다. 기본적으로 Node.js는 require()를 하는 경우 현재의 node_modules에서 먼저 찾고 없으면 부모 디렉터리로 가면서 계속 node_modules를 찾아서 의존성을 처리하기 때문에 우선순위상 자신의 디렉터리 하위에 있는 것을 먼저 사용하게 된다.

이는 보통 의존성 관리에서 골치 아프게 발생하는 버전 충돌 문제를 잘 해결했지만 대신 비효율적인 부분과 약간의 문제를 발생시켰다.

위에서 버전이 다른 경우 각각 설치되어서 괜찮다고 했지만 실제로는 같은 버전의 모듈을 사용한다고 하더라도 각각 설치된다. 예를 들어 l@v1.0.0을 의존성을 가진 모듈이 10개 있다면 10번 설치가 된다. 큰 문제는 아니지만 불필요한 부분인 것은 확실하다.

또한, 윈도우에서 경로의 길이를 최대 260자까지만 사용할 수 있는데 npm은 node_modules 모듈의 계층이 계속 깊어질 수 있는 구조이므로 이 260자 제한을 넘길 가능성이 꽤 높다.(난 윈도우 사용자가 아니라 이 문제를 만나면 어떤 일이 발생하는지는 잘 모르겠다.) 참고로 작년 play.node에서 Kat하고 얘기할 때 npm 사용자 중 42%가 윈도우 사용자라고 했다.(말도 안 돼!)

npm 3에서는 의존성 모듈을 계층적으로 설치하지 않고 가능한 한 계층을 없애서(flat) 설치한다. 앞에서 본 예제처럼 프로젝트가 ab 모듈에 의존성을 가지고 있는데 각 모듈이 l, m 모듈에 의존성을 가지고 있다면 npm 3는 다음과 같이 설치한다.

node_modules/
├── a/
├── b/
├── l/
└── m/

위에서 본 npm 2에서 설치된 모습과 비교하면 그 차이가 명백하다.

node_modules/
├── a/
│   └── node_modules/
│       └── l/
└── b/
    └── node_modules/
        └── m/

그러면 ab가 다른 버전의 모듈 l을 사용하는 경우에는 어떻게 될까? 즉, al@1.0.0을 사용하고 bl@2.0.0을 사용하는 경우에는 다음과 같이 설치된다.

node_modules/
├── a/
├── b/
│   └── node_modules/
│       └── l/  # v2.0.0
└── l/   # v1.0.0 

설명하면 a를 설치하면서 l@1.0.0node_modules/ 아래 설치하고 b를 설치할 때 l@2.0.0을 설치하려고 보니까 루트에 이미 l@1.0.0가 설치되어 있으므로 b 하위에 설치한다. require('l')을 할 때의 동작 방식은 같으므로 a모듈에서는 자신의 하위에 node_modules가 없으므로 상위의 l(v1.0.0)을 사용하고 b는 하위에 l이 있으므로 v2.0.0을 사용해서 의존성이 충돌하는 문제가 발생하지 않는다. 그리고 이미 npm v2로 모듈을 설치한 뒤 v3로 올린다고 하더라도 설치된 폴더의 모양만 다를 뿐 동작은 같으므로 문제가 발생하지 않는다.(지우고 다시 설치하면 물론 npm v3의 구조로 설치가 된다.)

여기서 l@2.0.0을 사용하는 모듈 c를 추가로 설치하면 b와 마찬가지로 이미 다른 버전의 l 모듈이 루트에 있으므로 자신의 하위에 설치해서 다음과 같이 된다.

node_modules/
├── a/
├── b/
│   └── node_modules/
│       └── l/  # v2.0.0
├── c/
│   └── node_modules/
│       └── l/  # v2.0.0
└── l/   # v1.0.0 

이 상황에서 l@1.0.0을 사용하는 모듈 d를 설치하면 l@1.0.0이 이미 설치되어 있으므로 다음과 같이 된다.

node_modules/
├── a/
├── b/
│   └── node_modules/
│       └── l/  # v2.0.0
├── c/
│   └── node_modules/
│       └── l/  # v2.0.0
├── d/
└── l/   # v1.0.0 

a모듈을 새 버전으로 업데이트했는데 이제 l@1.0.0 대신 l@2.0.0을 사용한다면 루트의 l@1.0.0가 설치되어 있고 d가 사용 중이므로 자신의 하위에 다시 설치해서 다음과 같이 된다.

node_modules/
├── a/
│   └── node_modules/
│       └── l/  # v2.0.0
├── b/
│   └── node_modules/
│       └── l/  # v2.0.0
├── c/
│   └── node_modules/
│       └── l/  # v2.0.0
├── d/
└── l/   # v1.0.0 

여기서 d도 업데이트를 했더니 l@1.0.0 대신 l@2.0.0을 사용한다면 이제 루트의 l@1.0.0을 사용하는 모듈이 아무도 없으므로 l@1.0.0을 지우고 l@2.0.0을 루트에 설치한다.

node_modules/
├── a/
│   └── node_modules/
│       └── l/  # v2.0.0
├── b/
│   └── node_modules/
│       └── l/  # v2.0.0
├── c/
│   └── node_modules/
│       └── l/  # v2.0.0
├── d/
└── l/   # v2.0.0 

동작에는 문제가 없지만, 결과적으로 모든 모듈이 l@2.0.0을 사용하고 있지만 중복되어 여러 곳에 설치가 되어 있다. 이때 npm dedupe를 실행하면 중복되는 의존성 모듈을 정리하고 다음과 같이 설치한다.

node_modules/
├── a/
├── b/
├── c/
├── d/
└── l/   # v2.0.0 

이는 기존에 npm 2로 의존성 모듈을 설치한 때도 npm dedupe을 실행하면 의존성 중복을 정리해준다.

지금까지 npm v3에서 의존성을 설치하는 방식을 설명하면서 하나씩 설치하는 과정을 설명했는데 이 과정에서 루트에 l@1.0.0이 있으므로 각 모듈이 자신의 하위에 l@2.0.0을 설치했는데 만약 처음 설치한 al@2.0.0을 사용하고 각 모듈이 l@1.0.0을 사용했다면 루트에 l@2.0.0가 설치되고 하위에 l@2.0.0설치되는 형태가 되었을 것이다. 다시 말하면 설치되는 node_modules의 디렉터리 구조는 모듈을 설치하는 순서에 따라 달라진다. package.json에 의존성이 보통 알파벳 순서로 정렬되어 있으므로 이 순서에 따라 설치되는 순서에 따라 의존성을 해결하게 된다. 그래서 기존에 설치된 node_modules가 있다면 개발자마다 다른 구조의 node_modules를 가질 수 있지만, 코드 실행에는 아무런 영향을 주지 않는다.

peerDependencies

보통 package.json에서 dependenciesdevDependencies를 사용하지만 peerDependencies는 gulp, grunt 같은 도구의 플러그인을 제작할 때 사용한다. 예를 들어 플러그인이 gulp v3 이상에서만 동작할 때 gulp v3가 설치되어 있다는 전제가 필요하고 이는 소스에서 사용하는 dependencies과는 다른데 이를 peerDependencies라고 부른다. 예를 들어 Chai Assertions for PromisespeerDependencies"chai": ">= 2.1.2 < 4"라고 정의되어 있는데 npm install chai-as-promised로 설치하면 npm v2에서는 필요한 peerDependencies를 함께 설치한다.

$ npm ls
/Users/outsider/peer
├─┬ chai@3.5.0
│ ├── assertion-error@1.0.2
│ ├─┬ deep-eql@0.1.3
│ │ └── type-detect@0.1.1
│ └── type-detect@1.0.0
└── chai-as-promised@5.3.0

npm v3에서는 이전처럼 자동으로 설치하지 않고(peerDependencies가 꼬이면 피곤하다.) peerDependencies가 충족되지 않으면 다은과 같이 경고가 나타난다.

$ npm ls
/Users/outsider/peer
├── UNMET PEER DEPENDENCY chai@>= 2.1.2 < 4
└── chai-as-promised@5.3.0

npm ERR! peer dep missing: chai@>= 2.1.2 < 4, required by chai-as-promised@5.3.0


그 외 프로그레스바 등 몇 가지 변경사항이 있지만, 호환성 등에서 크게 문제 될 요소는 없다고 생각된다.

2016/07/26 03:50 2016/07/26 03:50