안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

자바스크립트 (JavaScript)

[JavaScript] package-lock.json 파헤치기 (npm 7 ~)

피그브라더 2022. 11. 10. 16:58

이번 포스팅은 npm을 이용하여 패키지를 관리할 때 사용하는 package-lock.json 파일의 내용을 파헤쳐본다. 대략적으로는 프로젝트에서 필요로 하는 각 패키지의 정확한 버전을 명시함으로써 설치 시기에 따라 설치되는 패키지의 버전이 달라지는 문제를 막는 수단 정도로 알고 있는 사람이 많을 것이다. 그렇다면 구체적으로 package-lock.json 파일의 내용은 구체적으로 어떤 모습을 하고 있는지, 그리고 이것과 관련하여 호이스팅(Hoisting)이란 무엇인지까지 함께 알아보자.

 

만약 npm이나 package.json, package-lock.json 파일에 대한 기본적인 이해가 없다면 아래 포스팅을 먼저 읽고 오기를 권장한다.

 

[JavaScript] Node.js와 npm(+ npx)의 개념

1. Node.js Chrome V8 JavaScript 엔진으로 빌드된 JavaScript 런타임을 의미한다. 런타임이란 해당 프로그래밍 언어로 작성된 코드가 구동되는 환경을 말한다. 브라우저가 대표적인 JavaScript 런타임이다. 결

it-eldorado.tistory.com

 

[JavaScript] package.json, package-lock.json

npm으로 Node.js 패키지들을 관리하는 프로젝트의 경우, 프로젝트 루트 폴더에는 Node.js 패키지들이 설치된 node_modules 폴더와 함께 package.json, package-lock.json 파일이 위치하고 있다. 이번 포스팅에서는

it-eldorado.tistory.com

 

1. package-lock.json 파일의 업데이트

package-lock.json 파일의 내용을 알아보기에 앞서, 먼저 해당 파일의 버전과 관련된 업데이트 사항을 짚고 넘어가야 한다.

 

npm 7부터 package-lock.json 파일의 내용이 업데이트되었다. 즉, npm 6까지는 package-lock.json 파일의 버전이 1이었지만, npm 7부터는 버전 2를 사용하도록 설정된다. 버전 1과 버전 2의 가장 큰 차이점은 의존성 트리를 표현하는 방식이 달라졌다는 것이다. 버전 2부터는 packages 프로퍼티가 새로 등장했고, 이것이 기존에 dependencies 프로퍼티가 하던 역할을 대신하기 시작했다.

 

다만 하위 호환을 위해 버전 2에서도 dependencies 프로퍼티를 그대로 남겨두었기 때문에, 버전 2의 package-lock.json 파일에서 packages 프로퍼티가 없다면 dependencies 프로퍼티를 대신 사용하게 된다. 반면에 버전 3의 경우 dependencies 프로퍼티를 아예 사용할 수 없고, packages 프로퍼티만 사용할 수 있다. 하위 호환을 고려하지 않아도 될 만큼 충분한 시간이 흘렀다고 판단된다면 아마도 기본 버전이 3으로 바뀌지 않을까 싶다. 아직까지는 버전 2가 기본이다.

 

참고로, package-lock.json 파일의 버전은 lockfileVersion 프로퍼티에 명시된다. (예시는 아래에서 보여줄 코드를 참조)

 

2. 상황 가정

my-project 프로젝트에 다음과 같이 최신 버전의 react 패키지와 1.0.0 버전의 loose-envify 패키지를 설치했다고 가정하자.

cd my-project
npm init
npm install react loose-envify@1.0.0

굳이 1.0.0 버전의 loose-envify 패키지를 설치하는 것은, 최신 버전의 react 패키지가 직접 의존하는 loose-envify 패키지의 버전과 다른 버전의 loose-envify 패키지를 설치함으로써 의존성 트리가 어떻게 형성되는지 살펴보기 위함이다. 이게 무슨 말인지는 아래 설명을 읽다 보면 이해가 될 것이다.

 

3. package.json 파일의 내용

위와 같이 패키지를 설치하였다면 package.json 파일의 내용은 대략 다음과 같을 것이다. (dependencies 프로퍼티 부분이 중요)

{
  "name": "my-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "loose-envify": "^1.0.0"
  }
}

my-project 프로젝트도 결국에는 1.0.0이라는 버전을 가지는 하나의 패키지이며, 이는 ^18.2.0을 만족하는 버전의 react 패키지와 ^1.0.0을 만족하는 버전의 loose-envify 패키지를 직접 의존한다. (^ 기호의 개념은 공식 문서의 Caret Ranges 설명을 참조)

 

4. package-lock.json 파일의 내용

본격적으로 package-lock.json 파일의 내용을 살펴보자. dependencies 프로퍼티는 구버전의 방식이므로 신버전의 방식에 해당하는 packages 프로퍼티 부분을 집중적으로 파헤쳐보도록 하자.

{
  "name": "my-project",
  "version": "1.0.0",
  "lockfileVersion": 2,
  "requires": true,
  "packages": {
    "": {
      "name": "my-project",
      "version": "1.0.0",
      "license": "ISC",
      "dependencies": {
        "loose-envify": "^1.0.0",
        "react": "^18.2.0"
      }
    },
    "node_modules/js-tokens": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.3.tgz",
      "integrity": "sha512-SfeDkKyjCWzOfBEyjRcmtt4GUejT68lG5DL3+AkWFsyB5sJLcVs/Ucxk5vNjeg0qmQ/Js4jPJTzeqpaDB/6ZVg=="
    },
    "node_modules/loose-envify": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.0.0.tgz",
      "integrity": "sha512-u/so5faDpWlTZQu3L67RSH6I5ShOyAGdNKJD4ckHNEJs/YoQTqgguXDKg1SVNpfYnbxo49oCSj69mu4Bsol31w==",
      "dependencies": {
        "js-tokens": "^1.0.1"
      }
    },
    "node_modules/react": {
      "version": "18.2.0",
      "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
      "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
      "dependencies": {
        "loose-envify": "^1.1.0"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/react/node_modules/js-tokens": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
    },
    "node_modules/react/node_modules/loose-envify": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
      "dependencies": {
        "js-tokens": "^3.0.0 || ^4.0.0"
      },
      "bin": {
        "loose-envify": "cli.js"
      }
    }
  },
  "dependencies": {
  	...
  }
}

 

[1] 프로젝트에 해당하는 my-project 패키지의 정보가 작성되어 있다. 이는 package.json 파일의 내용과 대응한다.

{
  "packages": {
    "": {
      "name": "my-project",
      "version": "1.0.0",
      "license": "ISC",
      "dependencies": {
        "react": "^18.2.0",
        "loose-envify": "^1.0.0"
      }
    }
  }
}

 

[2] my-project 패키지가 ^18.2.0을 만족하는 버전의 react 패키지와 ^1.0.0을 만족하는 버전의 loose-envify 패키지를 직접 의존하기 때문에, 18.2.0 버전의 react 패키지와 1.0.0 버전의 loose-envify 패키지를 설치하였다. 18.2.0 버전을 설치한 이유는 해당 조건을 만족하는 가장 최신 버전이기 때문이고, 1.0.0 버전을 설치한 이유는 npm install 명령어 실행 시 명시적으로 설치할 버전을 지정하였기 때문이다.

{
  "packages": {
    "node_modules/react": {
      "version": "18.2.0",
      "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
      "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
      "dependencies": {
        "loose-envify": "^1.1.0"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/loose-envify": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.0.0.tgz",
      "integrity": "sha512-u/so5faDpWlTZQu3L67RSH6I5ShOyAGdNKJD4ckHNEJs/YoQTqgguXDKg1SVNpfYnbxo49oCSj69mu4Bsol31w==",
      "dependencies": {
        "js-tokens": "^1.0.1"
      }
    }
  }
}

 

[3] 18.2.0 버전의 react 패키지가 ^1.1.0을 만족하는 버전의 loose-envify 패키지를 직접 의존하고 1.0.0 버전의 loose-envify 패키지가 ^1.0.1을 만족하는 버전의 js-tokens 패키지를 직접 의존하기 때문에, 1.4.0 버전의 loose-envify 패키지와 1.0.3 버전의 js-tokens 패키지를 설치하였다. 1.4.0 버전과 1.0.3 버전을 설치한 이유는 해당 조건을 만족하는 가장 최신 버전이기 때문이다. 이때, 1.0.0 버전의 loose-envify 패키지가 이미 설치되어 있지만 해당 버전이 ^1.1.0을 만족하지 않기 때문에 1.8.20 버전의 react 패키지 내부에 별도로 1.4.0 버전을 설치하였음을 알 수 있다(Hoisting).

{
  "packages": {
    "node_modules/react/node_modules/loose-envify": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
      "dependencies": {
        "js-tokens": "^3.0.0 || ^4.0.0"
      },
      "bin": {
        "loose-envify": "cli.js"
      }
    },
    "node_modules/js-tokens": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.3.tgz",
      "integrity": "sha512-SfeDkKyjCWzOfBEyjRcmtt4GUejT68lG5DL3+AkWFsyB5sJLcVs/Ucxk5vNjeg0qmQ/Js4jPJTzeqpaDB/6ZVg=="
    }
  }
}

 

[4] 1.4.0 버전의 loose-envify 패키지가 ^3.0.0 || ^4.0.0을 만족하는 버전의 js-tokens 패키지를 직접 의존하기 때문에, 4.0.0 버전의 js-tokens 패키지를 설치하였다. 이때, 1.0.3 버전의 js-tokens 패키지가 이미 설치되어 있지만 해당 버전이 ^3.0.0 || ^4.0.0을 만족하지 않기 때문에 18.2.0 버전의 react 패키지 내부에 별도로 4.0.0 버전을 설치하였음을 알 수 있다(Hoisting).

{
  "packages": {
    "node_modules/react/node_modules/js-tokens": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
    }
  }
}

 

5. 의존성 트리 (feat. 호이스팅)

지금까지 설명한 의존성을 그림으로 표현해보면 다음과 같은 의존성 트리가 그려진다. 의존성 트리는 결국 실제로 node_modules 폴더의 중첩 구조를 표현한다. 여기서 주목할 건 npm이 제공하는 호이스팅(Hoisting)이 적용되어 있다는 것이다. 의존성 트리에서의 호이스팅이란 트리의 깊이(= 폴더가 중첩된 깊이)를 최대한 줄여서 패키지의 설치나 검색이 효율적으로 이뤄질 수 있도록 하는 전략이다. 만약 호이스팅이 적용되지 않았다면 다음 그림에서 진한 남색의 노드로만 이뤄진 트리가 그려지겠지만, 호이스팅이 적용되었기 때문에 진한 남색의 노드 두 개가 연한 남색의 노드로 끌어올려진 구조의 트리가 그려진 것이다. (앞에서 package-lock.json 파일의 내용을 살펴볼 때 Hoisting이라고 짧게 언급하고 넘어갔던 부분이 바로 이것이다.)

 

 

참고로 이러한 호이스팅은 동일한 버전의 패키지를 중복 설치하지 않도록 막는 역할까지 수행한다. 만약 N1 버전의 P1 패키지와 N2 버전의 P2 패키지가 모두 N3 버전의 P3 패키지를 직접 의존한다면, N3 버전의 P3 패키지는 한 번만 설치되도록 한다. (만약 다른 버전의 P3 패키지가 필요 없다면 그 설치 위치는 아마 최상위 디렉토리일 것이다.) 위 그림에서는 표현되어 있지 않지만 중요한 개념이라 첨언하였다.
한편, 이러한 호이스팅은 깊이를 줄인다는 측면에서는 최적화의 장점이 있지만, 프로젝트가 직접 의존하지 않는 어떤 패키지를 직접 의존한 것처럼 불러와 사용할 수 있다는 혼란을 만든다는 단점이 있다. 이를 유령 의존성(Phantom Dependency)라 부른다.