Logo

패키지 잠금 파일 (package-lock.json, yarn.lock)

자바스크립트 프로젝트에서 개발을 하다 보면 이름에 lock🔒이 들어있는 package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lock와 같은 파일을 자주 접하게 되죠? 그런데 간혹 신입 개발자가 이 파일의 변경 사항을 커밋하지 않아서 팀원들에게 누가 되거나, 심지어 지웠다가 낭패를 보는 경우를 종종 볼 수 있는데요.

이번 포스팅에서는 무심코 지나치게 쉽지만 사실 매우 중요한 용도를 위해서 사용되는 패키지 잠금 파일(package locks)에 대해서 알아보도록 하겠습니다.

자바스크립트 패키지 매니저

우선 현재 자바스크립트에서 사용되고 있는 패키지 매니저에 대해서 간단히 짚고 넘어가겠습니다. 패키지를 프로젝트에 설치하거나 갱신 또는 삭제하는데 사용되는 도구를 패키지 매니저라고 하는데요. 현재 자바스크립트 커뮤니티는 npm과 Yarn이라는 두 가지 도구로 양분되어 있으며, Pnpm이나 Bun과 같은 새로운 패키지 매니저도 최근에 인기를 끌고 있습니다.

기본적으로 이러한 패키지 매니저는 모두 앞으로 설명드릴 패키지 잠금 기능을 지원하고 있는데요. npm은 package-lock.json 파일을, Yrn은 yarn.lock 파일을, Pnpm은 pnpm-lock.yaml 파일을, Bun은 bun.lockb를 패키지 잠금 파일로 사용합니다. 즉, 패키지 잠금 파일은 자신이 개발을 하는 프로젝트에서 어떤 패키지 매니저를 사용하느냐에 따라 달라지게 됩니다.

패키지 관리 매커니즘

패키지 잠금을 이해하려면 먼저 패키지 매니저가 프로젝트가 의존하는 패키지를 어떻게 관리해주는지 이해하는 것이 좋습니다.

어떤 패키지 매니저를 사용하든 해당 프로젝트의 메타 정보는 package.json 파일을 통해 관리가 되는데요. package.json 파일에는 해당 프로젝트가 의존하고 있는 모든 패키지 이름과 버전이 나열되어 있습니다. 일반적으로 설치되어야 하는지 패키지들은 dependencies 항목에, 그리고 개발할 때만 필요한 패키지들은 devDependencies 항목에 명시되죠.

package.json 파일에 대해서는 별도 포스팅에서 자세히 다루고 있으니 참고 바랍니다.

예를 들어, reactreact-dom 패키지에 의존하는 프로젝트의 package.json 파일의 모습은 대략 다음과 같을 것입니다.

package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "dependencies": {
    "react": "^16.8.2",
    "react-dom": "^16.8.2"
  }
}

이렇게 설치가 필요한 패키지들이 package.json 파일에 등록이 되어 있으면, 프로젝트의 모든 개발자는 패키지 매니저의 설치 커맨드 하나로 모든 패키지를 한번에 설치할 수 있습니다. 아래와 같이 프로젝트에서 사용하고 있는 패키지 매니저에 따라 설치 커맨드를 날리면 package.json 파일에 등록되어 있는 모든 패키지가 npm registry로 부터 다운받아져 node_modules 디렉터리에 저장됩니다.

  • npm
$ npm i

또는

$ npm install
  • Yarn
$ yarn

또는

$ yarn install

참고로, 설치가 끝나고 node_modules 디렉터리 내부에 들어가보면, reactreact-dom 패키지 외에도 생소한 패키지들을 볼 수 있으실 것입니다. 이러한 패키지들은 해당 프로젝트가 직접적으로 필요하지는 않지만, reactreact-dom 패키지가 필요로하기 때문에 간접적으로 의존하게 된 패키지입니다.

$ ls node_modules
js-tokens     loose-envify  object-assign prop-types    react         react-dom     react-is      scheduler

이렇게 package.json 파일만 있으면 해당 프로젝트가 의존하고 있는 모든 패키지를 설치할 수 있기 때문에, node_modules 디렉터리는 Git 저장소에 올라가지 않도록 보통 .gitignore 파일에 추가합니다.

자바스크립트 패키지를 설치할 때 사옹되는 npm install 명령어에 대해서는 별도 포스팅에서 더 깊게 다루고 있으니 참고 바랍니다.

설치 시점에 따라 달라지는 패키지 버전

그렇다면 package.json 파일만 있으면 프로젝트의 모든 개발자가 패키지 매니저를 이용해서 항상 동일한 버전의 패키지를 설치할 수 있을까요? 안타깝게도 모든 개발자가 정확히 같은 시각 동시에 패키지를 설치하지 않는 이상 개발자들은 서로 상이한 버전의 패키지를 설치할 확률이 발생하게 됩니다. 가장 큰 이유는 package.json 파일에 등록된 패키지의 버전이 ^~ 등을 이용해서 범위로 지정된 경우가 많기 때문인데요.

예를 들어, 위 예제와 같이 react 패키지가 "react": "^16.8.2"이라고 등록되어 있으면, SemVer 규칙에 따라 16.8.2 이상 17.0.0 미만의 범위로 버전이 지정됩니다. 맨 처음 프로젝트에 react 패키지를 설치한 개발자 A가 특정 버전을 지정하지 않았다고 가정하면, 당시 react 패키지의 최신 버전은 16.8.2 였을 것입니다. 만약에 추후 개발자 B가 npm iyarn 명령어를 통해 모든 패키지를 설치할 당시 react 패키지의 최신 버전이 16.8.3이였다면 개발자 B의 PC에는 16.8.3이 설치되어 있을 것입니다. 마찬가지로 프로젝트에 몇달 후에 합류한 개발자 C가 react 패키지의 최신 버전이 16.9.1일 때 같은 방법으로 모든 패키지를 설치했다면, 개발자 C의 PC에는 16.9.1이 설치되었을 것입니다. 게다가 프로젝트의 CI 서버는 배포할 때마다 매번 모든 패키지를 설치한다고 가정하면, 애플리케이션이 배포되는 서버에는 항상 그 당시 최신 버전의 react 패키지가 설치되어 사용될 것입니다.

  • package.json: ^16.8.2
  • 개발자 A의 PC: 16.8.2
  • 개발자 B의 PC: 16.8.3
  • 개발자 C의 PC: 16.9.1
  • CI/상용 서버: 16.10.0

이렇게 서로 다른 버전의 패키지를 설치해서 사용하는 개발자 간에 큰 혼선이 발생할 수 있습니다. (ex. 개발자 A의 PC에서 성공하는 테스트가, 개발자 B의 PC에서는 실패, 개발자 C의 PC에서만 특정 버그 발생) 또한, 실제 서버에 배포된 애플리케이션이 개발자 컴퓨터에서 돌아가는 애플리케이션과 100% 동일하게 작동한다는 보장이 없어지게 되어 디버깅시 매우 난감한 상황에 빠질 수 있습니다. 이러한 상황은 패키지 매니저에서 패키지 잠금이 지원되지 않던 시절에 매우 골칫거리였습니다.

이 문제를 해결하기 위해서 먼저 yarn에서 패키지 잠금을 지원하고, 나중에 npm에서도 패키지 잠금을 지원하게 되었습니다. 이제 패키지 잠금은 대부분의 패키지 매니저에서 지원하는 필수 기능이 되었습니다.

보안 위험

설치 시점에 따라 상이한 버전이 패키지가 설치될 수 있다는 것은 개발자 경험 뿐만 아니라 보안 측면에서도 잠재적인 위험 요소가 될 수 있습니다. 만약에 라이브러리 관리자가 어떤 이유로든 신규 버전에 악성 코드를 심거나 악의적인 기능을 추가하여 npm 패키지 저장소에 배포한다면 어떻게 될까요? 해당 라이브러리의 버전이 ^~를 이용하여 범위로 명시된 프로젝트에서는 해당 버전이 자동으로 설치될 것입니다.

이렇게 npm 패키지 저장소와 같은 소프트웨어 패키지나 라이브러리의 공급망을 통해서 공격하는 것을 Supply-chain Attack이라고 하며, 대규모로 파급될 수 있는 보안 문제를 초래할 수 있습니다.

패키지 잠금

이렇게 동일한 package.json 파일을 사용해도 시간과 장소에 따라서 서로 다른 버전의 패키지가 설치되는 문제는 패키지 잠금을 통해 해결할 수 있습니다. yarn이나 비교적 최근에 릴리즈된 npm을 사용해서 프로젝트에 새로운 패키지를 설치하면 package.json 파일에 해당 패키지가 등록될 뿐만 아니라 패키지 잠금 파일이 생성되는 것을 보실 수 있으실 겁니다. package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb와 같은 패키지 잠금 파일에는 해당 프로젝트에서 각 패키지가 최초 설치될 당시에 정확히 어떤 버전이었는지가 기록됩니다.

예를 들어, 예제 프로젝트에서 npm i를 실행하면 프로젝트 최상위 디렉터리에 다음과 같이 package-lock.json 파일이 생깁니다.

package-lock.json
{
  "name": "my-project",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "react": {
      "version": "16.11.0",
      "resolved": "https://registry.npmjs.org/react/-/react-16.11.0.tgz",
      "integrity": "sha512-M5Y8yITaLmU0ynd0r1Yvfq98Rmll6q8AxaEe88c8e7LxO8fZ2cNgmFt0aGAS9wzf1Ao32NKXtCl+/tVVtkxq6g==",
      "requires": {
        "loose-envify": "^1.1.0",
        "object-assign": "^4.1.1",
        "prop-types": "^15.6.2"
      }
    },
    "loose-envify": { ... 생략 ... },
    "object-assign": { ... 생략 ... },
    "prop-types": { ... 생략 ... }
  }
}

react 패키지 부분을 보면 현재 이 글을 쓰고 있는 당시 react의 최신 버전인 16.11.0이 설치된 것을 알 수 있습니다. 이렇게 한 번 package-lock.json 파일이 생성되면, 그 이후로는 npm i 커맨드를 실행했을 때, npm 저장소를 기준으로 최신 버전을 설치하지 않습니다. 대신에, 항상 package-lock.json 파일에 명시되어 있는 버전으로 패키지를 설치를 해주기 때문에, 설치 시점에 상관없이 항상 동일한 버전의 패키지가 설치되는 것을 보장받을 수 있습니다.

자, 이제 제가 이 package-lock.json 파일을 프로젝트의 Git 저장소에 올려두면, 다른 개발자들은 package.json 파일 뿐만 아니라 package-lock.json 파일까지 내려받게 될 것입니다. 그러면 앞으로 프로젝트의 모든 개발자의 PC뿐만 아니라 애플리케이션이 배포되는 서버까지도, npm 저장소에 배포된 최신 버전을 무시하고 package-lock.json에 기록된 버전 기준으로 패키지가 설치될 것입니다. 🤗

주의 사항

여러 개발자가 함께 작업하는 프로젝트에서 패키지 잠금 관련해서 불상사가 일어나지 않도록 각별히 주의해야합니다.

우선, 프로젝트를 최초 셋업하는 개발자는 패키지 잠금 파일을 Git 저장소에 반드시 올려서 다른 개발자들이 패키지 잠금 파일을 기준으로 패키지를 설치할 수 있도록 해야합니다. 그리고 패키지 잠금 파일은 패키지 매니저가 신규 패키지를 설치하거나 기존 패키지를 갱신/제거할 때마다 package.json과 자동으로 동기를 맞춰주기 때문에 개발자가 이 파일을 직접 수정해야 할 필요는 없으며 해서도 안 됩니다. 마지막으로 신규 패키지를 설치하거나 기존 패키지를 갱신/제거한 개발자는 package.json과 더불어 함께 업데이트된 패키지 잠금 파일을 반드시 커밋해야 합니다. 그렇게 하지 않으면 다른 개발자들이 npm iyarn를 실행했을 때, package.json는 그대로인데 package-lock.json만 업데이트되는 황당한 경우를 겪게 됩니다.

마치면서

이상으로 자바스크립트 프로젝트에서 package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb와 같은 파일이 왜 필요하고 어떻게 관리해야되는지 알아보았습니다. 패키지 잠금 파일에 대한 좀 더 자세하 설명은 각 패키지 매니저의 공식 레퍼런스를 참고 바라겠습니다.