개요
일하던중 npm install 시 어떻게 프로젝트의 node_modules 아래에 패키지형태로 설치되는지 그 과정이 궁금하여 찾아보았다. 전체프로세스 파악 및 코드레벨에서 살펴보고자함.
메모
전체 프로세스
프로세스 도식
graph TD A[npm install 실행] --> B[package.json 분석] B --> C[의존성 트리 구성] C --> D[패키지 메타데이터 조회] D --> E{캐시 확인} E -->|캐시 있음| G[캐시에서 로드] E -->|캐시 없음| F[원격 저장소에서 다운로드] F --> H[캐시에 저장] G --> I[무결성 검증] H --> I I --> J[node_modules에 압축해제]
캐시폴더 구조 (windows)

_cacache/ ├── content-v2/ │ └── sha512/ # 실제 패키지 컨텐츠 (해시로 저장) ├── index-v5/ # 메타데이터 인덱스 └── tmp/ # 임시 파일들
전체 코드
// 1. npm CLI 진입점 (npm/cli/lib/commands/install.js)
class Install extends ArboristWorkspaceCmd {
async exec(args) {
const arb = new Arborist(opts)
await arb.reify(opts) // 실제 설치 시작
}
}
// 2. 의존성 분석 (@npmcli/arborist)
class Arborist {
async reify(options) {
// 의존성 트리 구성
const tree = await this.buildIdealTree(options)
// 각 패키지 설치
for (const node of tree.target.values()) {
await this.fetchNode(node)
}
}
}
// 3. 패키지 다운로드 (npm/pacote)
async function fetch(spec, options) {
// 메타데이터 조회
const manifest = await this.manifest(spec, options)
// tarball URL 획득
const tarballUrl = manifest._resolved
// 다운로드 및 캐시 저장
const integrity = manifest._integrity
await this.tarball(tarballUrl, options)
}
// 4. 캐시 처리 (npm/cacache)
const cacache = {
async put(cachePath, key, data, opts) {
// 해시 계산
const integrity = ssri.fromData(data)
// 캐시 저장
await writeFile(contentPath(cachePath, integrity), data)
// 인덱스 업데이트
await index.insert(cachePath, key, integrity)
},
async get(cachePath, key) {
// 캐시 조회
const info = await index.find(cachePath, key)
// 데이터 읽기
const data = await readFile(contentPath(cachePath, info.integrity))
return { data, integrity: info.integrity }
}
}
// 5. 압축 해제 (npm/pacote/extract)
async function extract(spec, dest, opts) {
// 캐시에서 데이터 로드
const { data, integrity } = await cacache.get(opts.cache, key)
// 무결성 검증
if (!ssri.checkData(data, integrity)) {
throw new Error('Invalid data')
}
// node_modules에 압축 해제
await tar.x({
cwd: dest,
file: data,
strip: 1,
...opts
})
}
모듈 소스
캐시저장
// cacache의 내부 동작
const cacache = require('cacache')
// 1. 패키지 컨텐츠를 캐시에 저장
await cacache.put(cachePath, key, tarballData, {
algorithms: ['sha512'] // SHA-512 해시 사용
})
// 2. 캐시에서 컨텐츠 읽기
const { data, integrity } = await cacache.get(cachePath, key)파일 읽기 (이진파일)
// lib/content/read.js
const readFile = async (cachePath, integrity) => {
// 1. 해시값으로 파일 경로 생성
const path = contentPath(cachePath, integrity)
// 2. 바이너리 데이터로 읽기
const data = await fs.readFile(path)
// 3. 무결성 검증
if (ssri.checkData(data, integrity)) {
return data // 원본 tar.gz 바이너리 반환
}
}압축 해제
// lib/extract.js
const extract = async (spec, dest, opts) => {
// 1. 캐시에서 바이너리 데이터 읽기
const { data } = await cacache.get(cachePath, key)
// 2. 메모리상에서 tar 스트림 생성
const stream = new tar.Parse()
// 3. node_modules에 압축 해제
await tar.x({
cwd: dest,
file: data, // 바이너리 데이터를 직접 전달
strip: 1, // package 디렉토리 제거
...opts
})
}