
이전까지 패키지 관리에 대해서는 뭔가 부분적으로 이해하고 있었다.
왜냐하면 경험 자체가 패키지를 설치하거나, 만든 패키지를 배포하기 위한 버전 관리였기 때문이다.
이런 경험은 버전에 대해 심화적으로 살펴볼 필요가 없다.
그래서 버전정보와 관련한 것들은 틸드, 캐럿에 대해서만 어느정도 이해하고 있었다.
그러나 이번에 Typescript를 비롯한 여러 패키지의 버전을 업그레이드하는 경험을 할 수 있었고, 이때 학습한 것들을 글로 남기고자 한다.
yarn.lock파일
package.json은 어떤 패키지를 사용하는지 대략적인 정보만을 가지고 있다.
yarn.lock파일은 package.json에 기록된 라이브러리가 실제로 어떤 버전으로 설치된 것인지를 기록한 파일이다.
덕분에 여러 개발자가 같은 프로덕트를 개발할때 동일한 라이브러리를 설치할 수 있게 된다.
딱 여기까지가 알고 있던 내용이었다.
하지만 뭔가 버전 업데이트를 한다고 하면, lock파일을 좀더 깊게 살펴볼 필요가 있었다.
왜 package.json에 정확한 버전을 표기하지 않을까?
package.json을 들여다보면, 버전 정보를 정확하게 고정하지 않고 ^와 ~를 통해 버전을 정의한다.
참고로 ~(틸드)는 패치 버전에 대한 하위호환성을, ^(캐럿)은 마이너버전에 대한 하위호환성을 보장한다.
그런데 왜 이런식으로 설계 됐을까?
그냥 정확한 버전으로 package.json에 정의해버리면 어떤 문제가 있을까?
1. 의존성 충돌/중복번들링
아래와 같이 패키지를 사용한다고 가정하자.
"dependencies": {
"packageA": "1.2.3",
"packageB": "1.3.0"
}
여기서 packageA의 react는 16를 사용하고, packageB의 react는 18을 사용한다면 참조시 충돌이 일어나서 제대로 동작하지 않을 수 있다.
2. 중복 설치
1번 상황처럼 메이저 버전이 아니라, 패치버전만 다르다고 가정해보자.
패치버전은 그렇게 큰 변경이 아니므로 내용이 거의 같다. 하지만, 버전을 정확하게 고정시키면 같은 내용을 두 번 받아와야하므로 속도가 느려지게된다. 이런 문제를 ~나 ^를 사용해서 계산하는 형태로 하나로 통일할 수 있다.
그래서 두번 받아올걸 한번만 받아오게 하여 속도를 개선하는 것이다.
또한, 두 패키지를 모두 사용한다면, 거의 같은 내용의 두 가지 버전이 모두 번들에 포함되게 되어 번들사이즈가 커지게 된다.
3. 버전 업데이트의 용이성
yarn upgrade를 하면 하위호환성을 지키는 형태로, 업그레이드가 진행된다.
보통 패치나 마이너 업데이트는 인터페이스를 변경하지 않고, 버그를 잡는 업데이트이기 때문에 한번에 자동으로 업그레이드 할 수 있다면 한번에 관리하기 용이할 것이다.
하지만 고정된 버전으로 관리한다면, 일일히 해당 패키지를 확인하고 마이너버전, 패치버전에 맞게 하나씩 고정 버전으로 올려줘야한다.
별게 아닌 것 같지만 수십개라면 굉장히 비용이 드는 작업이다.
yarn.lock 살펴보기
이제 다시 yarn.lock파일을 살펴보자. yarn.lock은 아래와 같이 구성된다.
@some-package@^1.0.0: //package.json에 정의된 버전
version: "1.0.0" // 실제 설치된 버전
resolved "some url/HASH_VALUE" // 패키지가 다운된 URL+해시값
integrity "hash value" // 해시값
dependencies:
"의존 패키지": "^3.0.0" //some-package의 package.json에 정의된 버전"
첫번째 이름이 package.json에 정의된 버전이다.
이건 틸드와 캐럿을 통해 표기한 버전으로, 정확히 어떤 버전이 설치됐는지를 보기위해서는 version필드를 살펴보아야한다.
resolved필드는 해당 패키지를 설치한 URL+SHA-1해시값을 표기하는데, 뒤의 해시값이 해당 파일이 변조되지 않았는지를 검사한다. 해당 URL에서 .tgz파일을 받아오는데, 이 파일의 내용을 해시한 값이다.
integrity도 동일하게 .tgz를 해시한 값을 나타내는데 sha-512으로 해시한 값이다. 취약점이 개선된 해시함수를 써서 더 강력하다.(SHA-1은 취약점이 있어서 취약한 해시함수다.)
그래서 실질적인 해시값 검증은 integrity가 하고, resolved는 하나의 url이라고 볼 수 있다.
그리고 이 패키지가 사용하는 패키지들이 dependencies에 표기되어있다.
이 dependencies들은 @some-package@^1.0.0이 의존하고 있는 패키지들의 목록이며, 해당 패키지 package.json에 정의된 버전 정보가 기록되어있다. 따라서 이 버전은 정확한 버전이 아니다.
정확한 버전을 보려면 의존 패키지@^3.0.0으로 기록된 부분을 재귀적으로 확인하면된다.
만약 같은 라이브러리의 다른 두 가지 메이저 버전이 설치됐다면?
yarn.lock에서는 아래와 같이 표기되게 된다.
some-dash@^5.17.20:
version "5.17.20"
resolved "https://registry.yarnpkg.com/some-dash/-/some-dash-5.17.20.tgz"
integrity sha512-xxxx
dependencies:
other-lib "^1.2.3"
some-dash@^4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/lodash/-/some-dash-4.18.0.tgz"
integrity sha512-yyyy
dependencies:
other-lib "^1.3.0"
이걸 의도했다면 다행이지만, 이 상황 자체가 의도하지 않은 경우 일 수 있다.
보통은 의도하지 않았을 것이다. 의존성 자체는 하나를 바라보게 하는게 장기적으로 봤을 때 안정적이다.
이러한 경우 해결하는 방법은 두 가지로 볼 수 있다.
해당 메이저 버전을 사용하는 라이브러리 탐색하여 그 라이브러리를 업데이트하는 방법
some-dash를 5버전으로 통일한다고 하면, 아래와 같이 진행할 수 있다.
1. some-dash@4.18.0을 dependencies로 가지는 패키지를 yarn.lock에서 탐색
2. 찾은 패키지가 some-dash 5버전을 사용하는 버전이 있는지 확인
3. 그 버전으로 업데이트
package.json의 resolutions 필드 사용
yarn.lock이 아닌, package.json의 resolutions필드에 버전정보를 강제해서 모든 패키지가 이 버전을 사용하게 하는 방법이 있다.
{
"dependencies": {
...
},
"resolutions": {
"some-dash": "5.18.0"
}
}

이전까지 패키지 관리에 대해서는 뭔가 부분적으로 이해하고 있었다.
왜냐하면 경험 자체가 패키지를 설치하거나, 만든 패키지를 배포하기 위한 버전 관리였기 때문이다.
이런 경험은 버전에 대해 심화적으로 살펴볼 필요가 없다.
그래서 버전정보와 관련한 것들은 틸드, 캐럿에 대해서만 어느정도 이해하고 있었다.
그러나 이번에 Typescript를 비롯한 여러 패키지의 버전을 업그레이드하는 경험을 할 수 있었고, 이때 학습한 것들을 글로 남기고자 한다.
yarn.lock파일
package.json은 어떤 패키지를 사용하는지 대략적인 정보만을 가지고 있다.
yarn.lock파일은 package.json에 기록된 라이브러리가 실제로 어떤 버전으로 설치된 것인지를 기록한 파일이다.
덕분에 여러 개발자가 같은 프로덕트를 개발할때 동일한 라이브러리를 설치할 수 있게 된다.
딱 여기까지가 알고 있던 내용이었다.
하지만 뭔가 버전 업데이트를 한다고 하면, lock파일을 좀더 깊게 살펴볼 필요가 있었다.
왜 package.json에 정확한 버전을 표기하지 않을까?
package.json을 들여다보면, 버전 정보를 정확하게 고정하지 않고 ^와 ~를 통해 버전을 정의한다.
참고로 ~(틸드)는 패치 버전에 대한 하위호환성을, ^(캐럿)은 마이너버전에 대한 하위호환성을 보장한다.
그런데 왜 이런식으로 설계 됐을까?
그냥 정확한 버전으로 package.json에 정의해버리면 어떤 문제가 있을까?
1. 의존성 충돌/중복번들링
아래와 같이 패키지를 사용한다고 가정하자.
"dependencies": {
"packageA": "1.2.3",
"packageB": "1.3.0"
}
여기서 packageA의 react는 16를 사용하고, packageB의 react는 18을 사용한다면 참조시 충돌이 일어나서 제대로 동작하지 않을 수 있다.
2. 중복 설치
1번 상황처럼 메이저 버전이 아니라, 패치버전만 다르다고 가정해보자.
패치버전은 그렇게 큰 변경이 아니므로 내용이 거의 같다. 하지만, 버전을 정확하게 고정시키면 같은 내용을 두 번 받아와야하므로 속도가 느려지게된다. 이런 문제를 ~나 ^를 사용해서 계산하는 형태로 하나로 통일할 수 있다.
그래서 두번 받아올걸 한번만 받아오게 하여 속도를 개선하는 것이다.
또한, 두 패키지를 모두 사용한다면, 거의 같은 내용의 두 가지 버전이 모두 번들에 포함되게 되어 번들사이즈가 커지게 된다.
3. 버전 업데이트의 용이성
yarn upgrade를 하면 하위호환성을 지키는 형태로, 업그레이드가 진행된다.
보통 패치나 마이너 업데이트는 인터페이스를 변경하지 않고, 버그를 잡는 업데이트이기 때문에 한번에 자동으로 업그레이드 할 수 있다면 한번에 관리하기 용이할 것이다.
하지만 고정된 버전으로 관리한다면, 일일히 해당 패키지를 확인하고 마이너버전, 패치버전에 맞게 하나씩 고정 버전으로 올려줘야한다.
별게 아닌 것 같지만 수십개라면 굉장히 비용이 드는 작업이다.
yarn.lock 살펴보기
이제 다시 yarn.lock파일을 살펴보자. yarn.lock은 아래와 같이 구성된다.
@some-package@^1.0.0: //package.json에 정의된 버전
version: "1.0.0" // 실제 설치된 버전
resolved "some url/HASH_VALUE" // 패키지가 다운된 URL+해시값
integrity "hash value" // 해시값
dependencies:
"의존 패키지": "^3.0.0" //some-package의 package.json에 정의된 버전"
첫번째 이름이 package.json에 정의된 버전이다.
이건 틸드와 캐럿을 통해 표기한 버전으로, 정확히 어떤 버전이 설치됐는지를 보기위해서는 version필드를 살펴보아야한다.
resolved필드는 해당 패키지를 설치한 URL+SHA-1해시값을 표기하는데, 뒤의 해시값이 해당 파일이 변조되지 않았는지를 검사한다. 해당 URL에서 .tgz파일을 받아오는데, 이 파일의 내용을 해시한 값이다.
integrity도 동일하게 .tgz를 해시한 값을 나타내는데 sha-512으로 해시한 값이다. 취약점이 개선된 해시함수를 써서 더 강력하다.(SHA-1은 취약점이 있어서 취약한 해시함수다.)
그래서 실질적인 해시값 검증은 integrity가 하고, resolved는 하나의 url이라고 볼 수 있다.
그리고 이 패키지가 사용하는 패키지들이 dependencies에 표기되어있다.
이 dependencies들은 @some-package@^1.0.0이 의존하고 있는 패키지들의 목록이며, 해당 패키지 package.json에 정의된 버전 정보가 기록되어있다. 따라서 이 버전은 정확한 버전이 아니다.
정확한 버전을 보려면 의존 패키지@^3.0.0으로 기록된 부분을 재귀적으로 확인하면된다.
만약 같은 라이브러리의 다른 두 가지 메이저 버전이 설치됐다면?
yarn.lock에서는 아래와 같이 표기되게 된다.
some-dash@^5.17.20:
version "5.17.20"
resolved "https://registry.yarnpkg.com/some-dash/-/some-dash-5.17.20.tgz"
integrity sha512-xxxx
dependencies:
other-lib "^1.2.3"
some-dash@^4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/lodash/-/some-dash-4.18.0.tgz"
integrity sha512-yyyy
dependencies:
other-lib "^1.3.0"
이걸 의도했다면 다행이지만, 이 상황 자체가 의도하지 않은 경우 일 수 있다.
보통은 의도하지 않았을 것이다. 의존성 자체는 하나를 바라보게 하는게 장기적으로 봤을 때 안정적이다.
이러한 경우 해결하는 방법은 두 가지로 볼 수 있다.
해당 메이저 버전을 사용하는 라이브러리 탐색하여 그 라이브러리를 업데이트하는 방법
some-dash를 5버전으로 통일한다고 하면, 아래와 같이 진행할 수 있다.
1. some-dash@4.18.0을 dependencies로 가지는 패키지를 yarn.lock에서 탐색
2. 찾은 패키지가 some-dash 5버전을 사용하는 버전이 있는지 확인
3. 그 버전으로 업데이트
package.json의 resolutions 필드 사용
yarn.lock이 아닌, package.json의 resolutions필드에 버전정보를 강제해서 모든 패키지가 이 버전을 사용하게 하는 방법이 있다.
{
"dependencies": {
...
},
"resolutions": {
"some-dash": "5.18.0"
}
}