어느 날인가부터 GitHub에서 내 저장소에 security vulnerability issue가 있다는 메일이 자꾸 날아오기 시작했다. 범인은 바로 예전에 Ethereum dApp 튜토리얼을 따라하면서 만들었던 Truffle 프레임워크 기반 웹앱이었다.

알려진 취약점이 포함된 과거 버전 라이브러리가 package-lock.json에 포함되어 있다고 했다.

해결은 어렵지 않았다. package-lock 파일에 포함된 해당 버전 넘버를 권장하는 수준 이상으로 설정해 주면 된다. 다만 이렇게 되면 높은 버전의 라이브러리가 의존하는 또 다른 라이브러리와 호환되지 않을 수 있고, 또 그 라이브러리도 의존하고 있는 다른…. (그만!)

그런데 이상한 것은, 문제가 된 mem 라이브러리가 package.json에는 포함되지 않았다는 사실이었다. 사실 npm install 명령어는 package.json 파일만 존재하면 정상적으로 실행된다. npm install 명령의 결과로 생성되는 것이 package-lock.json인 것이다. 그러면 그냥 package-lock.json을 날려버리고 package.json에서 다시 시작하면 되지 않을까?

고민하다가 package-lock 파일에 대해 찾아본 결과, 충격적인(?) 진실을 알 수 있었다.

package-lock.json은 필요하다

package-lock.json은 npm이 node_modules 폴더의 내용, 또는 package.json을 수정할 때 자동으로 생성되고, 정확한 의존성 트리 정보를 포함하고 있다.

이 말은, package.json에 저장되어 있는 정보는 사실 ‘정확하지는 않다’는 것이다. 의존성은 리스트 형태가 아니라 트리 형태이다. 다시 말해서 A가 의존하는 B 모듈이 또다시 C 모듈에 의존할 수 있고, 그 이상의 depth도 가능하다는 것이다. 그러므로 B 모듈(직접적 의존)의 버전 정보 뿐만 아니라 C 모듈(간접적 의존)의 버전 정보까지 보존하기 위해서는 package-lock.json 파일이 필요하다.

공식 문서에서는 다음과 같은 이유로 package-lock.json을 소스 저장소에 함께 커밋하도록 권장하고 있다.

This file is intended to be committed into source repositories, and serves various purposes:

  • Describe a single representation of a dependency tree such that teammates, deployments, and continuous integration are guaranteed to install exactly the same dependencies.
  • Provide a facility for users to “time-travel” to previous states of node_modules without having to commit the directory itself.
  • To facilitate greater visibility of tree changes through readable source control diffs.
  • And optimize the installation process by allowing npm to skip repeated metadata resolutions for previously-installed packages.

또 다른 존재: npm-shrinkwrap.json

그런데 문서를 읽는 중 흥미로운 사실을 발견했다. 바로 npm-shrinkwrap.json의 존재다. package-lock은 publication을 위한 용도는 아니기 때문에, top-level package, 즉 내가 현재 ‘보고 있는’ 패키지가 아니고 의존하는 패키지의 경우에는 package-lock 파일의 내용은 무시된다.

npm-shrinkwrap은 기본적으로 package-lock과 똑같은 파일이지만, npm 저장소로 publication 할 때에도 포함된다. 즉, npm-shrinkwrap이 있는 패키지를 설치하면 이 패키지가 의존하는 다른 라이브러리들도 동일한 버전을 설치하게 되는 것이다. 그러나 이는 CLI 툴, 또는 production package을 배포할 때 빼고는 권장되지 않는다. 이 패키지를 의존하는 다른 패키지들에게 강제로 특정 버전을 설치하도록 제한하기 때문이다.