초기 NPM 버전은 순차적 설치 방식
을 사용했다.
예를 들어, A, B, C 세 개의 패키지를 설치한다고 가정해 보자
- A 패키지 다운로드
- A 패키지 설치
- B 패키지 다운로드
- B 패키지 설치
- C 패키지 다운로드
- C 패키지 설치
이 방식에서는 각 패키지의 설치가 완료될 때까지 다음 패키지의 설치를 시작하지 않는다.
Yarn은 처음부터 병렬 설치
를 지원했다.
같은 세 개의 패키지를 설치할 때
- A, B, C 패키지 동시 다운로드 시작
- 다운로드 완료된 순서대로 설치 시작
이 방식은 네트워크 대역폭을 최대한 활용하고, CPU와 디스크 I/O를 효율적으로 사용한다.
예를 들어, A 패키지가 크고 다운로드에 시간이 오래 걸린다면, 그 동안 B와 C의 다운로드와 설치가 진행될 수 있다.
NPM 5
이후 버전에서는 Yarn과 유사한 병렬 설치를 도입했다.
그러나 초기 구현에서는 Yarn만큼 효율적이지 않았다
Yarn의 캐싱 시스템은 매우 공격적이라고 볼 수 있다.
패키지를 처음 다운로드할 때, 전역 캐시
에 저장한다. 이 캐시는 버전별로 관리된다.
예를 들어, lodash@4.17.20
을 설치한다고 가정해 보자
- Yarn은 먼저
로컬 캐시
확인.- 캐시에 없다면 다운로드하고 캐시에 저장.
- 다음에 같은 버전의 lodash가 필요할 때, 네트워크 요청 없이 캐시에서 직접 가져옴.
또한, Yarn은 오프라인 미러링
을 지원한다. 이는 모든 필요한 패키지를 로컬에 미리 다운로드해 놓고, 완전히 오프라인 상태에서도 설치할 수 있게 한다.
NPM도 캐싱
을 지원하지만, 초기 버전에서는 덜 효율적이었다.
NPM 5
이후 개선되었지만, 여전히 일부 시나리오에서 Yarn보다 덜 효율적일 수 있다.
NPM의 캐시는 패키지 이름
과 버전
을 기반으로 하는 폴더 구조
를 사용한다.
캐시된 패키지를 사용할 때, NPM은 무결성 검사
를 수행하는데, 이 과정이 추가적인 시간을 필요로 할 수 있다.
Yarn의 잠금 파일은 매우 상세하다.
1. 각 패키지의 정확한 버전
2. 의존성 트리
3. 무결성 체크섬
등을 포함한다. 이 정보는 매우 구조화되어 있어 빠르게 읽고 처리할 수 있다.
예를 들어, yarn.lock 파일의 일부분은 다음과 같을 수 있다:
lodash@^4.17.20:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
이 상세한 정보 덕분에 Yarn은 의존성 트리를 빠르게 재구성하고 설치할 패키지를 정확히 결정할 수 있다.
NPM 5
에서 도입된 package-lock.json도 유사한 기능을 한다.
그러나 초기 버전
에서는 일부 일관성 문제가 있었고, Yarn의 잠금 파일만큼 효율적이지 않았다.
NPM의 잠금 파일 예:
{
"name": "lodash",
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
}
Yarn은 네트워크 요청을 매우 효율적으로 관리한다. 요청을 큐에 넣고 우선순위를 지정하며, 동시에 여러 요청을 처리한다. 또한, 네트워크 오류에 대한 재시도 메커니즘이 잘 구현되어 있어, 일시적인 네트워크 문제를 잘 처리한다.
예를 들어, 100개의 패키지를 설치해야 한다면
- Yarn은 이를 여러 그룹으로 나눈다 (예: 10개씩 10그룹)
- 각 그룹을
병렬
로 다운로드 시작.- 다운로드가 완료된 패키지부터 즉시 설치 시작
- 네트워크 오류가 발생하면
자동
으로 재시도.
초기 NPM 버전은 이러한 최적화가 부족했다.
각 패키지를 개별적
으로 처리하고, 네트워크 오류 처리도 덜 효율적이었다.
최신 버전에서는 많이 개선되었지만, 여전히 일부 시나리오
에서 Yarn보다 덜 효율적일 수 있다.
Yarn은 의존성 트리를 구성할 때 매우 효율적인 알고리즘을 사용한다. 이는 결정론적
알고리즘으로, 같은 입력에 대해 항상 같은 출력을 생성한다.
Yarn의 의존성 해결 알고리즘은 다음과 같은 단계로 작동한다.
Yarn은 먼저 모든 패키지의 의존성 정보를 수집하여 방향성 비순환 그래프(DAG)를 구축한다. 각 노드는 패키지를, 엣지는 의존성 관계를 나타낸다.
각 패키지가 요구하는 의존성의 버전 범위를 수집한다. 예를 들어:
A 패키지: "C": "^1.0.0"
B 패키지: "C": "^2.0.0"
각 패키지에 대해 가능한 모든 버전의 목록을 생성한다. 이 때 semver(Semantic Versioning)
규칙을 적용한다
Yarn은 SAT(Boolean Satisfiability Problem) solver를 사용하여 모든 버전 제약 조건을 동시에 만족시키는 조합을 찾는다.
a) 각 패키지 버전을 boolean 변수로 변환
b) 버전 제약 조건을 논리식으로 표현
c) SAT solver를 사용하여 모든 조건을 만족하는 해를 찾음
여러 가능한 해결책 중에서 Yarn은 다음 기준을 사용하여 최적의 해결책을 선택한다.
a) 최신 버전 선호
b) 중복 설치 최소화
c) breaking changes 가능성 최소화
3. 잠금파일 사용과 중복되는 내용이지만 알고리즘 관점으로 본다면 최종 결정된 의존성 트리를 yarn.lock
파일에 기록한다. 이 파일은 정확한 버전 정보
와 무결성 해시
를 포함한다.
구체적인 예시
패키지 A
와 B
가 모두 C
에 의존하지만 다른 버전을 요구하는 경우:
A@1.0.0
└── C@^1.0.0
B@1.0.0
└── C@^2.0.0
- Yarn이 의존성 그래프 구축.
C
의 가능한 모든 버전(예: 1.0.0, 1.1.0, 2.0.0, 2.1.0) 나열.- SAT solver를 사용하여 다음 논리식을 해결: (C@1.0.0 OR C@1.1.0) AND (C@2.0.0 OR C@2.1.0)
- 가능한 해결책: C@1.1.0과 C@2.1.0 설치
- 최적화: 최신 버전인 C@2.1.0을 선택하고, A 패키지에 대해 호환성 경고 발생.
최종 의존성 트리
root
├── A@1.0.0
├── B@1.0.0
└── C@2.1.0
이 과정은 밀리초
단위로 매우 빠르게 실행되며, 수천 개의 패키지가 있는 복잡한 프로젝트에서도 효율적으로 작동한 Yarn의 이러한 알고리즘은 의존성 해결 과정을 매우 빠르고 결정론적
으로 만들어, 항상 동일한 환경에서 동일한 결과를 보장한다.
초기 NPM 버전은 이러한 복잡한 의존성 해결에 더 많은 시간이 걸렸다.
최신 버전에서는 개선되었지만, 특히 매우 복잡한 의존성 트리에서는 여전히 Yarn보다 느릴 수 있다.
Yarn은 가능한 경우 의존성을 '플랫하게' 설치하려고 시도한다. 이는 중복 설치를 최소화하고 디스크 공간을 절약한다.
예를 들어
A
├── C@1.0.0
└── B
└── C@1.1.0
이를 다음과 같이 평탄화할 수 있다.
A
├── C@1.1.0
└── B
전통적인 방식에서는 B의 두 버전을 모두 설치하지만, Yarn은 가능한 경우 하나의 버전
만 설치하려고 시도한다. 이는 설치 시간과 디스크 공간을 절약한다.
NPM도 최근 버전에서 이와 유사한 기능을 제공하지만, 초기에는 이러한 최적화가 없었다.
Yarn은 메모리 사용을 매우 효율적으로 관리한다. 대규모 프로젝트에서도 메모리 사용량이 급격히 증가하지 않는다.
예를 들어, 1000개의 패키지를 설치할 때
- Yarn은 필요한 정보만 메모리에 유지
- 사용하지 않는 데이터는 신속하게 메모리에서 해제
- 가비지 컬렉션을 효율적으로 관리하여 memory leaking 방지
Yarn은 글로벌 캐시
를 사용하여 다운로드한 패키지를 저장한다. 이 캐시는 보통 사용자의 홈 디렉토리에 위치한다 (예: ~/.yarn/cache
).
각 패키지 버전은 고유한 식별자(해시)를 가지고 캐시에 저장된다.
예: react-16.8.6-994e41b6.tar.gz
GC는 다음 상황에서 자동으로 또는 수동으로 실행될 수 있다.
yarn install
실행 시 (자동)yarn cache clean
명령어 실행 시 (수동)사용 중인 패키지 식별
yarn.lock
파일을 분석yarn.lock
파일도 선택적으로 검사 (설정에 따라)미사용 패키지 식별
캐시 디렉토리
의 모든 파일을 스캔삭제 대상 결정
(pruneOffsetInDays)
삭제 실행
삭제 대상
으로 표시된 패키지들을 캐시에서 제거
전체 GC 대신 특정 조건에 따라 부분적으로 GC를 실행할 수 있다.
병렬 처리
: 여러 캐시 항목을 동시에 처리하여 GC 속도 향상증분 GC
: 전체 캐시를 한 번에 처리하지 않고, 일부분씩 처리
하여 시스템 부하 감소잠금 파일
: GC 실행 중 다른 Yarn 작업이 캐시를 수정하지 못하도록 잠금롤백 메커니즘
: GC 중 오류 발생 시 캐시 상태를 이전으로 복원할 수 있는 기능yarn config
세팅으로 GC 관련 설정을 조정할 수 있다.
cacheFolder
: 캐시 위치 지정cacheMaxAge
: 캐시 항목의 최대 유지 기간pruneOffsetInDays
: 미사용 패키지 삭제 전 대기 기간예시 명령어:
yarn cache clean # 전체 캐시 정리
yarn cache clean package-name # 특정 패키지의 캐시만 정리
초기 NPM 버전은 대규모 프로젝트에서 메모리 사용이 비효율적이었다.
이는 특히 제한된 리소스의 환경(예: CI/CD
파이프라인)에서 문제가 될 수 있었다.
최신 버전에서는 개선되었지만, 여전히 일부 시나리오에서 Yarn보다 더 많은 메모리를 사용할 수 있다.
이러한 모든 요소들이 결합되어 Yarn이 NPM보다 일반적으로 더 빠른 성능을 보인다.
그러나 NPM도 지속적으로 개선되고 있어, 최신 버전에서는 그 격차가 많이 줄어들었다고는 한다.
실제 성능 차이는 프로젝트의 크기
, 의존성의 복잡성
, 사용 중인 NPM과 Yarn의 버전
등에 따라 달라질 수 있다.대규모 프로젝트나 복잡한 의존성 구조를 가진 프로젝트에서는 Yarn의 장점이 더 두드러질 수 있다.