오늘날 최고의 모노레포 솔루션의 장단점을 살펴봅시다.
아래 링크를 통해 전체 시리즈를 살펴보세요.
이 글은 Nx
, PNPM
, Turborepo
의 기능, 성능, 프로젝트 적합성을 비교하는 시리즈의 일부입니다.
Nx
와 Turborepo
의 내부 작동 방식과 주요 기능을 살펴본 후, 이제 PNPM
에 주목해 보겠습니다. PNPM
의 워크스페이스만으로도 우리의 요구 사항을 충족하고 개발 워크플로를 혁신할 수 있을 만큼 강력할까요?
다시 한 번 말씀드리지만, 저희의 궁극적인 목표는 개발을 간소화하고 코드베이스 관리를 강화할 수 있는 도구인 모노레포 챔피언을 선정하는 것입니다.
최고의 모노레포가 빛나길 바랍니다! 우리가 함께 정복할 도전은 다음과 같습니다.
전투는 계속됩니다! 우리가 함께 정복할 도전 과제는 다음과 같습니다.
다음 단계가 궁금하신가요? 함께 알아보시죠! 🚀 🌟
pnpm
(Performant NPM)은 속도, 효율성 및 디스크 공간 사용량에 대한 새로운 접근 방식을 취함으로써 npm
및 Yarn
과 같은 기존 패키지 관리자와 차별화됩니다.
🔳 pnpm
은 패키지를 글로벌 CAS(콘텐츠 주소 지정 가능 스토리지)에 저장합니다. CAS 디렉터리를 찾으려면 pnpm store path
.
PNPM 로컬 스토어(작성자 이미지)
저장소(CAS)를 방문하면 모든 콘텐츠가 해시 이름으로 레이블이 지정되어 있는 것을 확인할 수 있습니다.
그리고 해시 파일 중 하나를 열면 종속성의 실제 내용이 표시됩니다.
PNPM 해시 파일 콘텐츠(작성자 이미지)
pnpm
에서 파일은 파일 이름이 아닌 콘텐츠를 기준으로 저장 및 검색됩니다. 각 파일에는 식별자 역할을 하는 고유한 해시(Git 커밋 해시와 유사)가 할당됩니다. 이 해시는 파일의 콘텐츠를 기반으로 생성되므로 중복된 파일은 동일한 해시를 갖게 됩니다.
그런 다음 패키지를 설치할 때 pnpm
은 먼저 글로벌 스토어(CAS)에 동일한 해시를 가진 파일이 이미 존재하는지 확인합니다.
pnpm
은 프로젝트의 node_modules/.pnpm
폴더에서 저장소(CAS)의 기존 파일에 대한 하드 링크를 생성합니다.pnpm
이 파일을 다운로드하여 CAS에 저장한 다음 하드 링크를 생성합니다.
https://x.com/HemSays/status/1434921646083563525/photo/1
🔵 종속성의 각 버전은 물리적으로 저장 폴더(CAS)에 한 번만 저장되므로 단일 소스를 제공하고 상당한 양의 디스크 공간을 절약할 수 있습니다.
https://blog.logrocket.com/javascript-package-managers-compared/
🔳 하드 링크는 원본 파일과 동일한 inode(파일의 고유 식별자)를 공유하므로 디스크의 동일한 데이터 블록을 직접 가리킵니다.
예를 들어 document.txt
라는 파일과 report.txt
라는 하드 링크가 있는 경우 두 이름 모두 동일하고 정확한 파일 콘텐츠를 가리킵니다. document.txt
또는 report.txt
중 하나를 수정하면 두 데이터 모두 변경됩니다.
https://www.scaler.com/topics/hard-link-and-soft-link-in-linux/
이 하드 링크 전략은 디스크 공간을 절약할 뿐만 아니라 파일을 복사하는 패키지 관리자에 비해 설치 및 업데이트 속도를 획기적으로 높여줍니다.
🔳 그런 다음 모든 패키지를 node_modules/.pnpm
에 하드 링크한 후(CAS -> node_modules/.pnpm
) 심볼릭 링크를 생성하여 중첩 종속성 그래프 구조를 구축합니다.
node_modules
└── .pnpm
├── pretty-format@27.5.1
│ └── node_modules
│ └── react-is -> ../../react-is@17.0.2 // symlink
├── pretty-format@28.1.3
│ └── node_modules
│ └── react-is -> ../../react-is@18.3.1 // symlink
├── prop-types@15.8.1
│ └── node_modules
│ └── react-is -> ../../react-is@16.13.1 // symlink
├── rc-util@5.43.0_react-dom@18.2.0_react@18.2.0
│ └── node_modules
│ ├── react-is -> ../../react-is@18.3.1 // symlink
│ ├── react-dom -> ../../react-dom@18.2.0 // symlink
│ └── react -> ../../react@18.2.0 // symlink
├── react-is@16.13.1
├── react-is@17.0.2
├── react-is@18.3.1
├── react-dom@18.2.0
└── react@18.2.0
심볼릭 링크를 사용하는 이유는 무엇인가요? 세 가지 주요 이유 때문에 필수적입니다.
✔️ 호환성: Node.js
와 많은 도구는 종속성이 node_modules
내에 중첩되기를 기대합니다. 심볼릭 링크는 이러한 중첩 구조의 착각을 불러일으키는 동시에 효율성을 위해 하드 링크를 활용합니다.
✔️ 유연성: 심볼릭 링크를 사용하면 여러 패키지에 동일한 종속성의 다른 버전이 필요할 수 있는 복잡한 종속성 시나리오를 pnpm
에서 처리할 수 있습니다.
✔️ 효율성: 심링크는 파일을 복제하지 않고, 필요한 중첩 구조를 생성하여 디스크 사용량을 낮게 유지합니다.
🔳 패키지에 피어 종속성가 있는 경우 pnpm
은 종속성 그래프에서 더 위에 설치된 패키지에서 이러한 종속성이 해결되도록 합니다. 다음 예시를 살펴보겠습니다.
node_modules
└── .pnpm
├── pretty-format@27.5.1
│ └── node_modules
│ └── react-is -> ../../react-is@17.0.2
├── pretty-format@28.1.3
│ └── node_modules
│ └── react-is -> ../../react-is@18.3.1
├── prop-types@15.8.1
│ └── node_modules
│ └── react-is -> ../../react-is@16.13.1
├── rc-util@5.43.0_react-dom@18.2.0_react@18.2.0
│ └── node_modules
│ ├── react-is -> ../../react-is@18.3.1
│ ├── react-dom -> ../../react-dom@18.2.0
│ └── react -> ../../react@18.2.0
├── react-is@16.13.1
├── react-is@17.0.2
├── react-is@18.3.1
├── react-dom@18.2.0
└── react@18.2.0
pnpm
은 설치를 복제하는 대신 올바른 버전의 종속성을 가리키는 심볼릭 링크를 생성하므로 시간, 대역폭 및 디스크 공간을 절약할 수 있습니다.
기존 node_modules
설정(예: npm
또는 Yarn classic
)에서 여러 패키지가 동일한 종속성의 다른 버전에 의존하는 경우 해당 종속성은 node_modules
에 중복됩니다.
node_modules
├── pretty-format@27.5.1
│ └── node_modules
│ └── react-is@17.0.2
├── pretty-format@28.1.3
│ └── node_modules
│ └── react-is@18.3.1 (first one)
├── prop-types@15.8.1
│ └── node_modules
│ └── react-is@16.13.1
└── rc-util@5.43.0
└── node_modules
├── react-is@18.3.1 (second one)
├── react-dom@18.2.0
└── react@18.2.0
보시다시피 react-is@18.3.1
은 여러 번 중복되어 불필요한 디스크 공간을 차지합니다.
🔳 node_modules/.pnpm
폴더 내 각 종속성 폴더 이름에는 다음과 같은 세부 정보가 인코딩됩니다.
rc-picker
4.6.9
rc-picker@4.6.9_date-fns@2.30.0_dayjs@1.11.11_luxon@3.4.4_moment@2.30.1_react-dom@18.2.0_react@18.2.0__react@18.2.0
).
PNPM 종속성 메타데이터(작성자 이미지)
...
// https://github.com/react-component/picker/blob/master/package.json#L85
"peerDependencies": {
"date-fns": ">= 2.x",
"dayjs": ">= 1.x",
"luxon": ">= 3.x",
"moment": ">= 2.x",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"peerDependenciesMeta": {
"date-fns": {
"optional": true
},
"dayjs": {
"optional": true
},
"luxon": {
"optional": true
},
"moment": {
"optional": true
}
}
...
이 세심한 구성은 pnpm
이 종속성 호환성을 보장하고 잠재적인 충돌을 해결하는 데 도움이 됩니다.
🔳 이 단순화된 스키마는 pnpm
이 종속성을 관리하는 방법을 요약한 것입니다.
PNPM Dependency Management - Schema
Central Store (CAS)
│
├── package1@version1
│ ├── package.json
│ ├── index.js
│ └── ... (other files)
├── package2@version2
│ ├── package.json
│ └── ...
└── ... (other packages)
Project Structure (node_modules/.pnpm)
│
├── package1@version1 (Hard Link)
│ └── node_modules
│ ├── dependency1 -> ../../../dependency1@versionX (Symlink)
│ ├── dependency2 -> ../../../dependency2@versionY (Symlink)
│ └── ...
├── package2@version2 (Hard Link)
│ └── node_modules
│ └── ...
└── ...
.pnpm
디렉터리 내의 각 패키지의 메인 폴더는 글로벌 스토어(CAS)에서 하드 링크됩니다.node_modules
폴더 내에서 심볼릭 링크는 피어 종속성의 올바른 버전을 가리키는 데 사용됩니다.🔳 다음 표는 pnpm
과 경쟁사의 기능을 비교한 것입니다.
https://pnpm.io/feature-comparison
🔳 아래 벤치마크는 시간 성능과 다양한 상황에서 경쟁사 대비 pnpm
의 순위를 보여줍니다.
https://pnpm.io/benchmarks#lots-of-files
https://pnpm.io/benchmarks#lots-of-files
단일 프로젝트를 관리할 때 pnpm
의 효율성은 잘 알려져 있습니다. 하지만 서로 연결된 여러 프로젝트를 처리할 때 어떻게 확장할 수 있을까요? pnpm
워크스페이스을 자세히 살펴보고 여러 프로젝트 종속성을 동일한 효율로 관리할 수 있는지 알아봅시다.
🔳 워크스페이스의 루트에는 pnpm-workspace.yaml
파일이 있어야 합니다.
다음은 pnpm-workspace.yaml
파일의 내용 예시입니다.
packages:
- 'packages/*'
catalog:
'@babel/parser': ^7.24.7
'@babel/types': ^7.24.7
'estree-walker': ^2.0.2
'magic-string': ^0.30.10
'source-map-js': ^1.2.0
'vite': ^5.3.3
여기를 살펴보시면 워크스페이스 사용의 실제 예시 코드를 확인할 수 있습니다.
🔳 워크스페이스의 루트에 .npmrc
가 있을 수도 있습니다. .npmrc
파일에 워크스페이스에 대한 많은 구성 옵션을 추가할 수 있습니다.
pnpm 워크스페이스 구성(작성자 이미지)
다음은 .npmrc
파일의 내용 예시입니다.
# https://github.com/withastro/astro/blob/main/.npmrc
# 중요! 새 버전이 레지스트리에 있어도 `astro`를 설치하지 마십시오.
prefer-workspace-packages=true
link-workspace-packages=true
save-workspace-protocol=false # 이렇게 하면 예제에 `workspace:` 접두사가 붙지 않습니다.
🔳 pnpm
워크스페이스 내에서 패키지를 참조하려면 두 가지 주요 옵션이 있습니다.
✔️ 개별 package.json
파일 내에 워크스페이스 패키지의 별칭을 생성한 다음 다른 패키지의 package.json
파일에서 이 별칭을 종속성으로 참조할 수 있습니다.
다음은 MUI의 실제 예시입니다. 두 개의 횡단 패키지인 mui/system
과 mui/types
를 정의합니다.
// https://github.com/mui/material-ui/blob/next/packages/mui-system/package.json#L2
{
"name": "@mui/system",
"version": "6.0.0-beta.1",
"private": false,
"author": "MUI Team",
...
// https://github.com/mui/material-ui/blob/next/packages/mui-types/package.json#L2
{
"name": "@mui/types",
"version": "7.2.14",
"private": false,
"author": "MUI Team",
이 패키지는 다음과 같이 참조됩니다.
....
"dependencies": {
"@mui/core-downloads-tracker": "workspace:^",
"@mui/system": "workspace:*",
"@mui/types": "workspace:^",
"@mui/utils": "workspace:*",
...
},
"devDependencies": {
"@mui/internal-babel-macros": "workspace:^",
"@mui/internal-test-utils": "workspace:^",
...
별칭으로 작성된 종속성의 특정 버전 또는 범위를 지정할 수도 있습니다.
{
"dependencies": {
"foo": "workspace:*",
"bar": "workspace:~",
"qar": "workspace:^",
"zoo": "workspace:^1.5.0" // 특정 버전 지정
}
}
✔️ 상대 경로: 모노레포 내에서 상대 경로를 사용하여 워크스페이스 패키지를 참조할 수도 있습니다. 예를 들어, “foo”: “workspace:../foo"
는 종속성을 선언하는 패키지와 상대적인 형제 디렉터리에 있는 foo
패키지를 참조합니다.
🔳 작업 영역 패키지를 게시할 준비가 되면 pnpm
은 로컬 workspace:
의존성 참조를 표준 SemVer(Semantic Versioning) 범위로 자동 변환합니다. 이렇게 하면 게시된 패키지를 다른 프로젝트에서 원활하게 사용할 수 있으며, 심지어 pnpm
을 사용하지 않는 프로젝트에서도 사용할 수 있습니다.
예를 들어, 다음과 종속성 같은 종속성이 있습니다.
{
"dependencies": {
"foo": "workspace:*", // 워크스페이스의 모든 ‘foo’ 버전
"bar": "workspace:~", // ~1.5.0(1.5.x에 해당)
"qar": "workspace:^", // ^1.5.0(1.x.x와 호환)
"zoo": "workspace:^1.5.0" // 위와 같음
}
}
위 종속성은 아래와 같이 변환됩니다.
{
"dependencies": {
"foo": "1.5.0", // 정확한 버전
"bar": "~1.5.0",
"qar": "^1.5.0",
"zoo": "^1.5.0"
}
}
이제 게시된 패키지를 설치하는 모든 사람은 워크스페이스가 설정되어 있지 않더라도 올바른 버전의 종속성을 얻게 됩니다.
🔴 pnpm
작업 영역에는 내장된 버전 관리 기능(예: lerna 또는 npm)이 포함되어 있지 않지만 Changesets 및 Rush와 같은 기존 도구와 쉽게 통합할 수 있습니다.
⚫ pnpm
작업 영역의 순환 종속성으로 인해 스크립트 실행 순서가 예측할 수 없게 될 수 있습니다. 설치 중에 이러한 순환이 감지되면 pnpm
에서 경고를 표시합니다. 문제가 있는 패키지는 pnpm
에 의해 식별될 수도 있습니다. 이 경고가 발생하면 관련 package.json
파일에서 dependencies
, optionalDependencies
및 devDependencies
에 선언된 종속성을 검사해야 합니다.
다행히도 우리는 순환 종속성에 대해 이야기하고 있습니다. 다음 섹션에서는 종속성 그래프, 유향 비순환 정렬 등 pnpm
에 대해 자세히 살펴보겠습니다.
🔳 pnpm
은 그래프 관리를 위해 다음과 같은 내부 라이브러리를 사용합니다.
graph-builder
: 이 라이브러리는 pnpm-lock.yaml
파일에서 종속성 그래프를 구성하는 역할을 담당합니다.graph-sequencer
: 이 라이브러리는 그래프에서 패키지를 정렬하기 위해 유향 비순환 정렬 알고리즘을 구현합니다.🔳 PNPM은 주로 DAG를 사용하여 패키지 간의 종속성 관계를 모델링합니다. 이 그래프는 pnpm-lock.yaml
파일의 dependencies
, devDependencies
및 optionalDependencies
필드를 기반으로 구성됩니다(const currentPackages = currentLockfile?.packages ?? {}
).
// https://github.com/ahaoboy/pnpm/blob/main/deps/graph-builder/src/lockfileToDepGraph.ts#L91C1-L108C1
export async function lockfileToDepGraph (
lockfile: Lockfile,
currentLockfile: Lockfile | null,
opts: LockfileToDepGraphOptions
): Promise<LockfileToDepGraphResult> {
const currentPackages = currentLockfile?.packages ?? {}
const graph: DependenciesGraph = {}
const directDependenciesByImporterId: DirectDependenciesByImporterId = {}
if (lockfile.packages != null) {
const pkgSnapshotByLocation: Record<string, PackageSnapshot> = {}
await Promise.all(
Object.entries(lockfile.packages).map(async ([depPath, pkgSnapshot]) => {
if (opts.skipped.has(depPath)) return
// TODO: 최적화: 이 정보는 이미 pkgSnapshotToResolution()에서 반환할 수 있습니다.
const { name: pkgName, version: pkgVersion } = nameVerFromPkgSnapshot(depPath, pkgSnapshot)
const modules = path.join(opts.virtualStoreDir, dp.depPathToFilename(depPath), 'node_modules')
const packageId = packageIdFromSnapshot(depPath, pkgSnapshot, opts.registries)
...
const dir = path.join(modules, pkgName)
const depIsPresent = !('directory' in pkgSnapshot.resolution && pkgSnapshot.resolution.directory != null) &&
currentPackages[depPath] && equals(currentPackages[depPath].dependencies, lockfile.packages![depPath].dependencies)
let dirExists: boolean | undefined
if (
depIsPresent && isEmpty(currentPackages[depPath].optionalDependencies ?? {}) &&
isEmpty(lockfile.packages![depPath].optionalDependencies ?? {})
) {
dirExists = await pathExists(dir)
if (dirExists) {
return
}
brokenModulesLogger.debug({
missing: dir,
})
}
let fetchResponse!: Partial<FetchResponse>
if (depIsPresent && equals(currentPackages[depPath].optionalDependencies, lockfile.packages![depPath].optionalDependencies)) {
if (dirExists ?? await pathExists(dir)) {
fetchResponse = {}
} else {
brokenModulesLogger.debug({
missing: dir,
})
}
}
...
pnpm-lock.yaml
의 예제를 살펴봅시다.
packages:
"@adobe/css-tools@4.4.0":
resolution:
{
integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==,
}
"@ampproject/remapping@2.3.0":
resolution:
{
integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==,
}
engines: { node: ">=6.0.0" }
"@ant-design/colors@7.1.0":
resolution:
{
integrity: sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==,
}
"@ant-design/cssinjs@1.21.0":
resolution:
{
integrity: sha512-gIilraPl+9EoKdYxnupxjHB/Q6IHNRjEXszKbDxZdsgv4sAZ9pjkCq8yanDWNvyfjp4leir2OVAJm0vxwKK8YA==,
}
peerDependencies:
react: ">=16.0.0"
react-dom: ">=16.0.0"
---
"@jest/reporters@28.1.3":
resolution:
{
integrity: sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==,
}
engines: { node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0 }
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
"@jest/reporters@29.7.0":
resolution:
{
integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==,
}
engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 }
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
---
devDependencies:
"@babel/core":
specifier: "=7.18.6"
version: 7.18.6
"@babel/eslint-parser":
specifier: "=7.24.6"
version: 7.24.6(@babel/core@7.18.6)(eslint@9.0.0)
"@babel/preset-env":
specifier: "=7.23.6"
version: 7.23.6(@babel/core@7.18.6)
babel-jest:
specifier: "=29.7.0"
version: 29.7.0(@babel/core@7.18.6)
🔳 findCycle
함수는 방향이 지정된 그래프에서 주기를 감지하는 고전적인 알고리즘입니다. 이 함수는 큐 기반 접근 방식(BFS, Breadth-First Search)을 사용하여 주어진 노드(startNode
)에서 시작하여 그래프를 탐색합니다. 탐색 중에 startNode
를 다시 만나면 사이클이 존재한다는 뜻입니다.
// https://github.com/pnpm/pnpm/blob/main/deps/graph-sequencer/src/index.ts#L99
function findCycle (startNode: T): T[] {
const queue: Array<[T, T[]]> = [[startNode, [startNode]]]
const cycleVisited = new Set<T>()
const cycles: T[][] = []
while (queue.length) {
const [id, cycle] = queue.shift()!
for (const to of graph.get(id)!) {
if (to === startNode) {
cycleVisited.add(to)
cycles.push([...cycle])
continue
}
if (visited.has(to) || cycleVisited.has(to)) {
continue
}
cycleVisited.add(to)
queue.push([to, [...cycle, to]])
}
}
if (!cycles.length) {
return []
}
cycles.sort((a, b) => b.length - a.length)
return cycles[0]
}
...
// https://github.com/pnpm/pnpm/blob/main/deps/graph-sequencer/src/index.ts#L66
const cycleNodes: T[] = []
for (const node of nodes) {
const cycle = findCycle(node)
if (cycle.length) {
cycles.push(cycle)
cycle.forEach(removeNode)
cycleNodes.push(...cycle)
if (cycle.length > 1) {
safe = false
}
}
}
chunks.push(cycleNodes)
}
🔳 종속성 그래프가 구성되면 pnpm
은 위상 정렬 알고리즘을 사용하여 패키지를 처리할 올바른 순서를 결정합니다. 이렇게 하면 패키지의 종속성이 항상 패키지 자체보다 먼저 처리되도록 보장합니다.
/**
* 노드 제한을 지원하면서 그래프에서 유향 비순환 정렬을 수행합니다.
*
* @param {Graph<T>} graph - 키는 노드이고 값은 간선의 가장자리인 맵으로 표현된 그래프입니다.
* @param {T[]} includedNodes - 정렬 프로세스에 포함해야 하는 노드의 배열입니다. 다른 노드는 무시됩니다.
* @returns {Result<T>} safe, chunk, cycles를 포함한 정렬 결과가 포함된 객체입니다.
*/
export function graphSequencer<T> (graph: Graph<T>, includedNodes: T[] = [...graph.keys()]): Result<T> {
// 모든 노드에 대해 빈 배열로 reverseGraph를 초기화합니다.
const reverseGraph = new Map<T, T[]>()
for (const key of graph.keys()) {
reverseGraph.set(key, [])
}
...
🔴 pnpm
의 주요 초점은 효율적인 종속성 관리에 있으며, DAG와 유향 비순환 정렬을 사용하여 패키지 종속성을 올바르게 설치하고 해결할 수 있습니다. 그러나 작업 실행(package.json
에 정의된 스크립트)과 관련하여 pnpm
은 본질적으로 엄격한 유향 비순환 순서를 강요하거나 Nx
또는 Turborepo
와 같은 도구와 같은 캐싱 메커니즘을 제공하지 않습니다.
✅ pnpm
에는 Nx
나 Turborepo
와 같은 작업 오케스트레이션 시스템이 내장되어 있지는 않지만, 워크스페이스의 여러 패키지에서 작업을 쉽게 실행할 수 있는 여러 명령어와 옵션을 제공합니다.
예시: 병렬 빌드 및 미리보기 스크립트
"scripts": {
"build": "pnpm --parallel --filter \"./**\" build",
"preview": "pnpm --parallel --filter \"./**\" preview"
},
이 예에서는 build
및 preview
스크립트가 워크스페이스 내의 모든 패키지에 걸쳐 병렬로 실행됩니다.
다음 표는 pnpm
, Nx
및 Turborepo
를 구분하는 기능에 대한 간략한 개요를 제공합니다.
PNPM vs NX vs Turbo(작성자 이미지)
간단히 말해 다음과 같이 말할 수 있습니다.
이러한 인사이트를 바탕으로, 저처럼 PNPM 워크스페이스와 Vite
, Vitest
, ESLint
와 같은 성능 좋은 도구를 결합하면 효율적인 모노레포 개발에 충분한지 궁금하실 수도 있습니다. Nx
나 Turborepo
와 같은 복잡한 모노레포 전용 도구에 의존하지 않고도 원활한 개발자 경험(DX)을 구현할 수 있을까요? 다음 섹션에서 이 질문에 대해 자세히 알아보겠습니다.
ESLint
: 강력한 조합?🔳 이 설정의 잠재력을 이해하려면 Nx
및 Turborepo
와 같은 모노레포 도구가 제공하는 핵심 이점을 기억해 두는 것이 좋습니다.
🔳 이제 pnpm Workspace + Vite + Vitest + ESLint
스택의 가치 제안을 분석해 보겠습니다.
✔️ pnpm
Workspace
pnpm
의 핵심 강점은 콘텐츠 주소 지정이 가능한 파일 시스템을 사용하여 효율적인 종속성 해결 및 저장에 있습니다. 따라서 설치 속도가 빨라지고, 디스크 사용 공간이 줄어들며, 안정성이 향상됩니다.Nx
나 Turborepo
만큼 포괄적이지는 않지만 pnpm
워크스페이스는 공유 종속성, 프로젝트 연결, 패키지 간 손쉬운 스크립트 실행과 같은 기본적인 모노레포 기능을 제공합니다.✔️ Vite
✔️ Vitest
✔️ 캐싱이 포함된 ESLint
pnpm
워크스페이스을 사용하면 모노레포 전체에서 ESLint 구성 및 규칙을 쉽게 공유하여 일관된 코드 품질을 보장할 수 있습니다.🔳 다음은 pnpm Workspace + Vite + Vitest + ESLint(캐싱 포함)
조합, Nx
및 Turborepo
간의 주요 차이점을 요약한 비교표입니다.
마법의 조합 vs Nx vs Turborepo (작성자 이미지)
Key
🔳 비교 표에 비추어 몇 가지 미리 생각해 보세요.
PNPM + Vite
가 좋은 선택이 될 수 있습니다.Nx
가 강력한 옵션입니다.Turborepo
가 PNPM + Vite
에 비해 몇 가지 이점을 제공할 수 있습니다.지금까지의 결과는 희망적이지만 아직 끝나지 않았습니다! 팀 개발과 지속적 통합을 위해 공유 캐시를 설정하여 PNPM + Vite
의 잠재력을 최대한 발휘할 수 있는 우회 방법을 함께 살펴보세요.
실제로 로컬 캐싱은 Vite
, Vitest
, ESLint
와 같은 도구에 내장된 캐싱 메커니즘으로 인해 PNPM + Vite
에서는 문제가 되지 않는 경우가 많습니다. 따라서 Nx
또는 Turborepo
에서 제공하는 추가 로컬 캐싱은 불필요할 수 있습니다. 모노레포 성능에 대한 진정한 과제는 PNPM의 고유한 구조로 인해 전략적인 캐시 관리가 필요한 CI 빌드를 최적화하는 데 있습니다.
CI 빌드 캐시에 대한 가능한 해결책은 Docker를 사용하는 것입니다.
🔳 핵심 아이디어는 기본 프로젝트 설정(Node.js
, PNPM
등)이 포함된 공유 Docker 이미지를 사용하고 각 CI 빌드 중에 리포지토리의 최신 코드로 이 이미지를 업데이트하는 것입니다.
🔳 다음은 기본 프로젝트 설정이 포함된 Docker 이미지의 예입니다.
FROM node:18
# 전역적으로 pnpm 설치
RUN npm install -g pnpm
# 작업 디렉토리 설정
WORKDIR /app
# 프로젝트 파일 복사
COPY package.json pnpm-workspace.yaml ./
# 의존성 설치
RUN pnpm 설치
# 기타 설정(선택 사항)
# (예: Vitest, ESLint 등과 같은 추가 도구 설치)
🔳 Docker 이미지가 빌드되고 my-project-base
와 같은 이름으로 레이블이 지정됩니다.
다음은 브랜치별 특정 빌드에 대한 GitHub 액션 워크플로우를 구성하는 간단한 예제입니다.
name: CI Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set environment variables
run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV
- name: Pull Docker image
run: docker pull my-project:${{ env.BRANCH_NAME }} || true
- name: Start container and update project
run: |
docker run -v $(pwd):/app my-project:${{ env.BRANCH_NAME }} sh -c " \
git fetch && \
git checkout ${{ env.BRANCH_NAME }} && \
pnpm install \
pnpm build
"
- name: Build and push Docker image
run: |
docker build -t my-project:${{ env.BRANCH_NAME }} .
# (Optional: push the updated image to a registry)
my-project:${{ env.BRANCH_NAME }}
)으로 태그된 Docker 이미지를 가져옵니다. 태그가 존재하지 않으면 예를 들어 가장 최근의
태그로 되돌아갑니다.my-project:${{ env.BRANCH_NAME }}
)으로 태그가 지정됩니다.🔳 다음은 병렬 풀 리퀘스트 처리를 위한 GitHub Actions 워크플로우를 구성하는 간단한 예시입니다.
name: CI Build
on:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set environment variables
run: echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
- name: Pull Docker image
run: docker pull my-project-base:latest || true
- name: Start container and update project
run: |
docker run -v $(pwd):/app my-project-base sh -c " \
git fetch && \
git checkout ${{ github.head_ref }} && \
pnpm install \
pnpm build
"
- name: Build and push Docker image
run: docker build -t my-project:pr-${{ env.PR_NUMBER }} .
# (선택 사항: 업데이트된 이미지를 레지스트리에 푸시)
my-project:pr-123
)로 태그된 Docker 이미지를 빌드하는 별도의 CI 작업을 트리거할 수 있습니다.🔳 이 솔루션의 주요 한계는 다음과 같습니다.
🔻 여러 팀 또는 프로젝트에서 동일한 기본 이미지를 사용하는 경우 한 팀에서 변경한 내용이 다른 팀의 캐시를 의도치 않게 무효화할 수 있습니다.
my-project-teamA-base
, my-project-teamB-base
).🔻 특히 종속성이 많은 모노레포의 경우 Docker 이미지가 상당히 커질 수 있습니다. 이러한 이미지를 밀고 당기는 데는 시간이 걸리고 상당한 대역폭을 소비할 수 있습니다.
pnpm prune
으로 사용하지 않는 종속성을 검토하고, Docker파일 명령을 전략적으로 정렬하고, 명령을 결합하고, .dockerignore
를 사용하는 것입니다. 또한 알파인 Linux 또는 배포되지 않는 이미지와 같은 경량 옵션을 사용하는 것도 고려해 보세요.🔻 캐시 무효화 및 정리 전략을 구현하여 오래된 캐시가 누적되어 저장 공간을 차지하지 않도록 해야 합니다.
✅ 일부 CI 플랫폼은 특정 도구(예: Yarn
또는 npm
또는 pnpm
)에 대한 기본 제공 캐싱을 제공합니다. 추가 최적화를 위해 이러한 기능을 Docker 캐싱과 함께 활용할 수 있습니다.
✅ PNPM + Vite
모노레포 내에서 수동 캐싱 전략(예: Docker 또는 사용자 정의 스크립트)에 의존하는 것의 중요한 장점 중 하나는 Nx
및 Turborepo
에서 제공하는 클라우드 기반 캐싱 솔루션과 관련된 잠재적 비용을 피할 수 있다는 것입니다.
pnpm
워크스페이스 구성과 캐싱 전략의 미묘한 차이를 살펴본 다음, pnpm
의 효과에 대한 기술적 평가를 살펴보겠습니다. 그런 다음 사례 연구와 실제 사례를 통해 실제 환경에서 어떻게 작동하는지 살펴보겠습니다. 시작하겠습니다!
🔳 장점: PNPM이 빛나는 곳
npm
또는 Yarn
에 비해 설치 및 종속성 관리 속도가 눈에 띄게 빠릅니다.🔳 좋지 않은 점: 개선이 필요한 부분
npm
이나 Yarn
에 익숙한 사용자에게는 약간의 적응이 필요할 수 있습니다.pnpm
이 어떻게 활용되고 있는지 살펴봅시다.🔳 pnpm
은 지난 몇 년 동안 꾸준히 인기를 얻고 있습니다.
https://npmtrends.com/pnpm-vs-yarn
https://npmtrends.com/pnpm-vs-yarn
https://npm-stat.com/charts.html?package=pnpm&package=yarn&package=npm&from=2021-01-04&to=2024-07-21
https://npm-stat.com/charts.html?package=pnpm&package=yarn&package=npm&from=2021-01-04&to=2024-07-21
🔳 pnpm
은 Microsoft와 같은 거대 기술 기업부터 Prisma와 같은 혁신적인 스타트업, 심지어 Rush 및 SvelteKit과 같은 영향력 있는 오픈 소스 프로젝트에 이르기까지 다양한 조직에서 널리 채택되고 있습니다.
🔳 GitHub의 수많은 프로젝트에서 pnpm
을 사용합니다.
🔳 GitHub의 수많은 프로젝트가 pnpm
워크스페이스을 사용합니다.
이러한 실제 현장에서의 모멘텀은 자바스크립트 생태계에서 pnpm
의 중요성이 커지고 있음을 강조하며, 기존 패키지 관리자에 대한 실행 가능하고 매력적인 대안으로서 그 입지를 확인해줍니다.
이제 상황을 살펴봤으니 이제 우리의 길을 선택할 차례입니다. 하나의 도구가 최고로 군림할까요, 아니면 하이브리드 접근 방식이 모노레포에 최적화된 전략일까요? 한번 봅시다!
다음은 의사 결정 과정에 도움이 되도록 PNPM workspace + Vite
, Nx
및 Turborepo
의 주요 차이점을 요약한 표입니다.
모노레포 도구 적합성(작성자 이미지)
분석 결과에서 알 수 있듯이 이상적인 모노레포 도구는 각 프로젝트의 특정 요구사항에 따라 크게 달라집니다. 그러나 특히 자체 코드 생성기인 비스트로를 개발하는 저희는 단순성, 유연성, 성능을 최우선 순위로 삼고 있으므로 Nx를 제외하여 선택의 폭을 좁힐 수 있습니다.
Nx는 분명 강력하지만 광범위한 기능으로 인해 현재 요구사항에 불필요한 복잡성을 유발하여 잠재적으로 성능에 영향을 미칠 수 있습니다. 저희는 툴을 완벽하게 제어하고, 비스트로를 워크플로에 쉽게 통합하며, 무엇보다도 높은 성능 표준을 유지할 수 있는 솔루션을 선호합니다.
따라서 유연성과 효율성이 뛰어난 PNPM Workspace + Vite
(및 기타 필수 도구)로 기준선을 설정할 것입니다. 빌드 성능을 향상해야 하는 경우, 전문화된 최적화 기능을 제공하는 Turborepo
를 쉽게 추가할 수 있습니다(기존 리포지토리에 추가하는 방법에 대한 Turborepo
의 가이드 참조).
이상으로 모노레포 도구의 세계에 대해 자세히 알아봤습니다! 지금까지 모노레포의 환경을 살펴보고, 내부 작동 방식을 자세히 살펴보고, 장단점을 따져봤습니다. 이제 결론을 내릴 시간입니다! 🌟
결론적으로 모노레포 도구를 살펴본 결과, 프로젝트의 요구 사항에 따라 PNPM, Nx, Turborepo가 각각 고유한 강점을 제공한다는 것을 알 수 있었습니다.
속도, 효율성, 단순성에 중점을 둔 PNPM은 다양한 시나리오에 적합한 다목적 솔루션으로 돋보입니다. 그러나 Nx는 복잡한 대규모 모노레포를 관리하는 데 탁월한 반면, Turborepo는 빌드 최적화에 우선순위를 둡니다.
유연성과 툴링에 대한 통제력 유지에 중점을 두었기 때문에 현재 요구 사항을 처리하고 사용자 지정 코드 생성기인 비스트로와 원활하게 통합할 수 있다는 확신을 가지고 PNPM Workspace + Vite를 기반으로 구축하기로 결정했습니다. Turborepo의 빌드 최적화 능력은 부인할 수 없지만, 프로젝트의 성장으로 인해 더 큰 성능 향상이 필요할 때 언제든지 배포할 수 있도록 계속 보유할 것입니다.
단순하고 직관적으로 유지하세요! 레오나르도 다빈치는 단순함이 궁극적인 정교함이라고 말했습니다. ❤️
이 시리즈를 통해 프로젝트에 가장 적합한 모노레포 도구를 결정하는 데 필요한 지식과 통찰력을 얻으셨기를 바랍니다.
만능 솔루션은 없으며, 다양한 조합을 실험해 보면 특정 요구사항에 맞는 완벽한 설정을 찾을 수 있다는 점을 기억하세요. 모노레포의 유연성을 활용하고 행복한 코딩을 즐겨보세요! 🌟
모노레포의 모험에 동참해 주셔서 감사합니다! 🚀
새로운 글과 새로운 모험으로 다시 만날 때까지! ❤️
제 글을 읽어주셔서 감사합니다.
저와 연락하고 싶으신가요?
GitHub에서 저를 찾을 수 있습니다: https://github.com/helabenkhalfallah