자바스크립트 환경에서 작업하시는 여러분. 안녕하세요. 오늘의 글은 아래의 항목의 질문을 가진 분들을 위한 글입니다. 아래의 항목에 해당되는 분이 아니라면, 그리 재미있는 글은 아닐거에요 ㅎ_ㅎ 한번 읽어보시겠어요.
이와 같은 질문들을 가진 분이라면 이 글은 도움이 될 것입니다.
글을 읽기 전에, 미리 이해하고 있으면 도움이 되는 지식들입니다.
package-lock.json은 의존성 트리 내 모든 중첩된 의존성들의 정확한 버전을 설치 시점에 기록하여, 동일한 환경에서 일관된 패키지 구성을 보장하는 파일이다.
package-lock.json 파일은 위와같이 정의내려볼 수 있습니다. 아직은 잘 와닿지 않겠지만, 저 문장들을 더욱 분명하게 이해할 수 있도록 돕는 것이 이 글의 목적입니다.
위의 문장들을 통해서 저희가 추측해볼 수 있는 사실은 3가지입니다. 기존의 package.json만으로는 부족하기 때문에
1) 중첩된 의존성들의 정확한 버전을 기록해야만 하는 이유가 생겼다는 것이고,
2) 그것도 설치시점에 기록을 해두어야 할 이유가 있다는 것이며,
3) 기존에는 일관된 패키지 구성이 어려웠다.
라는 것입니다.
아직 완벽히 이해가 되지는 않을거에요. 왜 정확한버전을 기록해야하는지? 그것도 설치시점에 기록해두어야하는 이유는 무엇인지? 일관된 패키지 구성이 어려운 이유는 무엇인지.
이 모든 이유를 파악하는데 핵심이 되는 지점은 버전 표기법인 'caret(^)표기'입니다. 이 표기법이 무엇이고, 이것으로부터 발생할 수 있는 문제가 무엇인지를 이해한다면, package-lock.json이 왜 필요하며, 이것의 역할이 무엇인지에 대해서 이해하게 될 것입니다.
먼저, Semantic Versioning(시맨틱 버저닝) 에 대해 다시 짚어볼 필요가 있습니다. 시맨틱 버저닝은 버전을 세 가지 숫자로 구분하는 방식입니다. 예를 들어, react@18.3.1 와 같은 버전이 있을 때, 점(.)을 기준으로 구분된 세 가지 숫자는 다음과 같은 구조를 가지고 있습니다:
18: 주버전 (Major)
3: 부버전 (Minor)
1: 수정버전 (Patch)
버전을 표기할 때, 이 세 가지 숫자는 각각 중요한 의미를 가지고 있습니다.
그런데, 여기서 중요한 것은 특정 버전을 다운로드할 때 '범위'를 명시할 수 있다 는 점입니다. 예를 들어, caret(^) 표기를 사용하면 주버전(Major)을 고정한 상태에서 부버전(Minor)과 수정버전(Patch)의 업데이트를 허용하는 범위를 지정할 수 있습니다.
react@^18.3.1이라고 명시한다면, 이 표시는 주버전(Major)인 18에서 하위 호환성을 유지하면서 부버전과(Minor) 수정버전(Patch)이 업데이트된 새로운 버전도 설치할 수 있다는 의미입니다. 즉, 18.3.2, 18.4.0, 18.9.5 등으로 업데이트될 수 있지만, 주버전이 변경되는 19.x.x는 설치되지 않습니다.
이처럼 caret(^) 표기를 사용하면 특정 범위 내에서 패키지의 유연한 업데이트가 가능해지지만, 문제는 이렇게 유연하게 업데이트되는 버전들이 팀 내 여러 개발자들이나 서버에서 일관되지 않게 설치될 수 있다는 점입니다. 이는 개발 환경이나 배포 환경에서 불일치로 인해 예기치 않은 버그나 문제가 발생할 수 있는 가능성을 높입니다.
예상되는 시나리오를 한번 생각해보겠습니다.
철수가 react@^18.3.1 라고 명시된 package.json을 보고 npm install을 할 때는 react의 minor 버전이 3이었습니다. 때문에 react@^18.3.x 버전의 의존성을 다운로드할 것입니다.
하지만 영희가 며칠 뒤 동일한 프로젝트에서 npm install을 실행할 때는 react의 minor 버전이 18.4.0으로 업데이트되었을 수 있습니다. 이 경우 영희는 react@18.4.0 버전을 다운로드하게 됩니다. 두 사람 모두 package.json에 명시된 대로 react@^18.3.1을 설치했지만, 서로 다른 버전의 react가 설치된 것입니다.
이러한 이유로 저희는 "설치된 시점의 의존성의 버전과 중첩된 의존성의 정확한 버전"을 기록할 이유가 생깁니다. 설치할 때마다 달라진 버전의 의존성을 설치할 가능성이 있기 때문이죠.
그래서 npm은 install이 실행되고 나면, 언제나 자동으로 package-lock.json 파일이 생성됩니다. 설치된 시점의 정확한 의존성의 버전들을 기록하기 위함입니다. 때문에 package-lock.json 파일은 반드시(거의 반드시) git 히스토리에 포함시켜 다른 개발자도 해당 파일을 바라볼 수 있게 해주어야 합니다.
npm은 install 명령어를 실행할 때, 먼저 package-lock.json 파일이 있는지를 확인하고, 있다면 그 파일을 기준으로 의존성을 설치합니다. 즉, package-lock.json에 기록된 정확한 버전의 패키지들이 설치되므로, 팀 내의 다른 개발자들이나 서버에서도 동일한 버전의 패키지를 설치하게 됩니다. 이는 모든 환경에서 일관된 패키지 구성을 보장하는 데 중요한 역할을 합니다.
만약 package-lock.json 파일이 없다면, npm은 package.json 파일을 기준으로 의존성을 설치하게 되며, 이 경우 위에서 설명한 것처럼 ^ 등의 범위 지정에 따라 서로 다른 버전의 패키지가 설치될 수 있습니다.
맞습니다. 만약 저희가 사용하는 의존성을 항상 정확하게 명시한 버전을 사용하고 싶으면, 범위 표기법을 사용하지 않고 그냥 명확한 버전을 명시하여 사용하면 됩니다. 하지만, 저희가 사용한 의존성이 사용하고 있는 또 다른 의존성에 대해서는 컨트롤을 할 수 있는 여지가 없습니다. 예를 들어 저희가 다운받은 의존성은 react인데 이 react가 또 다른 의존성을 의존하고 있고, 그때 caret(^) 표기법을 사용한다면요? 또 그 의존성 안에서 또 다른 의존성에 대해 caret(^)표기법을 사용한다면???
저희가 컨트롤할 수 있는 범위를 넘어서겠죠. 그렇기 때문에 그냥 설치한 시점에 다운로드 받은 의존성의 버전들을 정확하게 기록해놓을 수 있는 lock 파일을 사용하게 된 것입니다.
지금까지 약간 긴 호흡으로 달려온 것 같으니, 다시한번 정돈을 해보겠습니다.
저희는 Caret(^) 표기법을 이해함으로 package-lock.json이 필요해진 이유를 알아보았습니다. 정리하면 다음과 같습니다.
사실 lock 파일이 필요해진 이유는 이것뿐만이 아닐것이라고 생각합니다. 하지만, 제가 읽고 이해하는 가운데 가장 단순하게 정돈해볼 수 있는 수준으로 정리하여 이렇게 글로 적게 되었습니다. 이 정도면 lock 파일이 왜 필요한지에 대한 이해도는 조금 올라갔을 것이라고 생각합니다. 앞으로는 lock을 볼 때마다, 어렴풋하게 이해하고 있던 지점이 해소되기를 바라구요, 각종 버그를 해결하는 순간에 오늘 얻게 된 이 지식이 하나의 디딤돌이 되기를 바래봅니다. 🕺🏻