"모노레포" 관련 이야기를 하기 앞서 패키지 관리자인 pnpm을 선택한 이유
✏️ npm과 yarn 패키지 관리자 대신 pnpm을 선택한 이유 3가지
1. 디스크 공간의 혁신적인 절약 (가장 큰 장점) 💾
- 문제점 (npm, Yarn Classic): npm이나 yarn은 프로젝트마다 node_modules 폴더를 만들고, 필요한 모든 패키지 파일들을 그 안에 복사합니다. 만약 내 컴퓨터에 10개의 프로젝트가 있고, 모두 react 라이브러리를 사용한다면, react 패키지는 10번 복사되어 디스크에 저장됩니다. 이는 엄청난 디스크 공간 낭비입니다.
- pnpm의 해결책: pnpm은 패키지를 컴퓨터의 중앙 저장소(global store)에 단 한 번만 다운로드합니다. 그리고 각 프로젝트의 node_modules 폴더에는 실제 파일이 아닌, 이 중앙 저장소에 있는 파일을 가리키는 **'바로 가기(Symbolic Link)'**만 생성합니다.
비유 : 컴퓨터의 "바로가기(shortcut)" 아이콘과 같다. 100MB짜리 원본 프로그램은 C드라이브에 한 곳에 있고, 바탕화면이나 다른 폴더에는 그 원본을 가리키는 1KB짜리 "바로가기" 아이콘만 여러 개 만들어 놓는 것과 같은 원리이다.
2. 매우 빠른 설치 속도 ⚡
- 이유:
- 중복 다운로드 방지: 위에서 설명한 중앙 저장소 덕분에, 내 컴퓨터에 이미 설치된 적 있는 버전의 패키지는 다시 다운로드하지 않고 즉시 '바로 가기'만 만듭니다.
- 효율적인 프로세스: 의존성을 분석하고 패키지를 가져오는 과정을 매우 효율적으로 병렬 처리하여 설치 시간을 단축합니다.
- 효과: 새로운 프로젝트를 시작하거나 CI/CD 환경에서 의존성을 설치할 때 드는 시간이 크게 줄어들어 개발 생산성이 향상됩니다.
3. '유령 의존성(Phantom Dependencies)' 문제 원천 차단 👻
이것은 기술적으로 매우 중요한 장점이며, pnpm의 안정성을 보장하는 핵심 기능입니다.
- 문제점 (npm, Yarn Classic): 이 둘은 node_modules를 '플랫(flat)' 구조로 만듭니다. 예를 들어, 내 프로젝트가 A라는 패키지에만 의존하고, A 패키지가 내부적으로 B 패키지에 의존한다고 가정해봅시다. npm install을 하면 node_modules 최상위에 A와 B가 모두 설치됩니다. 이 때문에 내 프로젝트 코드에서 package.json에 명시하지도 않은 B 패키지를 import 해서 사용할 수 있게 됩니다. 이것을 **'유령 의존성'**이라고 합니다. 나중에 A 패키지가 업데이트되면서 더 이상 B를 사용하지 않게 되면, 내 프로젝트는 갑자기 에러를 내며 멈추게 됩니다.
- pnpm의 해결책: pnpm은 '논-플랫(non-flat)' 구조의 node_modules를 만듭니다. package.json에 직접 명시한 의존성의 '바로 가기'만 node_modules 최상위에 노출시킵니다. 그 의존성들이 내부적으로 사용하는 다른 패키지들은 격리된 다른 공간에 숨겨집니다.
- 효과: "내 package.json에 명시한 패키지만 사용할 수 있다"는 규칙이 강제됩니다. 이로 인해 프로젝트의 의존성 관계가 매우 명확해지고, 예기치 않은 버그가 발생할 확률이 크게 줄어듭니다. 이는 여러 패키지가 얽혀있는 모노레포 환경에서 특히 중요합니다.
그러면 이제 모노레포에 대해 알아보자!
일반적으로, 레포지토리 설계 방식에는 모노레포 or 멀티레포가 존재한다. 그리고 이러한 방식에는 아래와 같은 차이가 존재하는데,
🧩 모노레포(Monorepo)와 멀티레포(Multirepo)의 차이점
🔹 모노레포(Monorepo)
- 정의: 여러 프로젝트를 하나의 저장소에서 관리하는 방식
- 장점:
- 공통 모듈을 쉽게 공유하여 코드의 일관성을 유지할 수 있음
- 프로젝트 간의 의존성을 명확하게 관리할 수 있음
- 단점:
- 모든 프로젝트가 하나의 저장소에 모여 있어 빌드 시간이 길어질 수 있음
🔹 멀티레포(Multirepo)
- 정의: 각 프로젝트를 별도의 저장소로 관리하는 방식
- 장점:
- 프로젝트별로 독립적인 개발이 가능하며, 각 프로젝트의 특성에 맞게 최적화할 수 있음
- 프로젝트 간의 의존성을 줄일 수 있음
- 단점:
- 공통 모듈을 공유하기 어렵고, 프로젝트 간의 의존성 관리가 복잡해질 수 있음
🏗️ 합리적인 선택 기준
- 모노레포가 적합한 경우:
- 대규모 프로젝트를 관리할 때
- 공통 모듈을 많이 사용하는 경우
- 멀티레포가 적합한 경우:
- 소규모 프로젝트를 독립적으로 개발할 때
- 프로젝트 간의 의존성을 최소화하고자 할 때
🛠️ 모노레포 구축 비교
모노레포를 구축하는 방법은 여러가지 존재한다. 현재 글에서는 Turborefo 툴에 대한 설명을 중점적으로 할 예정이고, 아래의 링크를 통해 다양한 구축 방법을 확인해 볼 수 있다.
https://monorepo.tools/#workspace-analysis%EF%BB%BF
Monorepo Explained
Everything you need to know about monorepos, and the tools to build them.
monorepo.tools
⚡ Turborepo
정의: Vercel에서 운영하고 있는 JavaScript/TypeScript를 위한 모노레포 빌드 시스템이다.
- 장점:
- 초기 설정이 간편 및 Next.js(react)로 설정
- 병렬 처리 및 캐싱 전략을 통해 빌드 속도가 빠름
- 자동으로 패키지 의존성 탐지
⚡ Turborepo의 병렬 처리
turbo run build
내부적으로는 다음과 같이 병렬 실행됨:
build packages/ui
build packages/utils (동시에 실행)
⚡ Turborepo의 캐싱 전략 (가장 큰 장점)
1. 로컬 캐싱: 빌드 결과를 로컬에 캐시하여 동일한 작업을 반복할 때 빌드 시간을 단축
turbo run build
- 특정 패키지의 변경이 없다면, 해당 패키지의 출력 메세지에는 "캐시 사용됨" 메시지가 출력되고, 실제로 컴파일하지 않음
2. 원격 캐싱: Vercel의 캐싱 서버나 자체 캐싱 서버를 활용하여 CI/CD 환경에서 빌드 시간을 절약
과정: [빌드] → [원격 캐시에 저장] → [다른 사람 Pull] → [재빌드 생략]
- 빌드한 결과를 서버에 저장해두고, 팀원들이 다시 빌드하지 않고 바로 꺼내 쓰는 것
⚡ Turborepo의 Auto 패키지 의존성 탐지
워크스페이스 내 모든 프로젝트의 package.json 파일을 스캔하여 패키지 간의 관계를 파악합니다.
만약, apps/web/package.json에 다음과 같은 내용이 있다면:
// apps/web/package.json
"dependencies": {
"react": "18.3.1",
"ui": "workspace:*"
}
turoborepo는 "ui": "workspace:*"라는 부분을 보고, **"apps/web 프로젝트가 ui라는 이름의 로컬 패키지에 의존하고 있다"**는 사실을 인지합니다.
✨ 알아두기
1) 위와 같은 개별 package.json 안의 "workspace:*"
- workspace:*는 외부 npm 레지스트리가 아닌, 현재 모노레포 내부의 로컬 패키지를 참조하라는 의미입니다. 이때, ui는 해당 패키지의 name을 가리킵니다.
2) 최상위 루트에 존재하는 워크스페이스
- 워크스페이스의 '범위'를 정의
- 이 파일은 pnpm에게 "우리 모노레포는 apps 폴더와 packages 폴더 안에 있는 모든 프로젝트들로 구성되어 있어. 이들을 하나의 큰 단위로 관리해줘"라고 선언하는 역할을 합니다.
⚡ Turborepo의 태스크 파이프라인 설정(태스크 의존성 탐지)
위와 같이 패키지 간의 관계를 파악했다면, 다음은 작업(Task) 간의 관계를 정의할 차례입니다. 이는 주로 turbo.json의 pipeline 설정을 통해 이루어집니다.
어떻게 탐지(정의)하나요?
- turbo.json 파일에서 특정 작업이 다른 작업에 의존한다고 명시합니다. 이때 ^ (캐럿) 기호가 매우 중요하게 사용됩니다.
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {
"dependsOn": []
}
}
}
- "build": { "dependsOn": ["^build"] }
- ^build의 의미: "현재 패키지의 build를 실행하기 전에, 이 패키지의 패키지 의존성(위 1번에서 탐지한)들의 build 작업을 먼저 실행하라."
- "test": { "dependsOn": ["build"] }
- build의 의미 ( ^ 없음): "현재 패키지의 test를 실행하기 전에, 현재 패키지 내의 build 작업을 먼저 실행하라." (다른 패키지의 build는 신경 쓰지 않음)
- "lint": { "dependsOn": [] }
- lint 작업은 다른 어떤 작업에도 의존하지 않고 독립적으로 실행될 수 있음을 의미합니다.
요약: turbo run build 실행 시 일어나는 일
이 두 가지 의존성 탐지가 어떻게 함께 작동하는지 turbo run build 명령을 통해 살펴보겠습니다.
- 패키지 탐색: Turborepo는 워크스페이스에서 build 스크립트를 가진 모든 패키지를 찾습니다. (예: apps/web, packages/ui, packages/utils)
- 패키지 의존성 분석: package.json을 분석하여 web이 ui에 의존하고, ui와 utils는 서로 독립적이라는 사실을 파악합니다.
- utils → (독립)
- ui → (독립)
- web → ui
- 태스크 의존성 분석: turbo.json의 pipeline.build 설정을 확인합니다. dependsOn이 ^build로 되어있는 것을 봅니다.
- 최적의 실행 계획 수립:
- web을 빌드하려면 ^build 규칙에 따라 web의 패키지 의존성인 ui의 build가 먼저 끝나야 합니다.
- ui와 utils는 아무것에도 의존하지 않으므로, 즉시 병렬로 빌드를 시작할 수 있습니다.
- ui의 빌드가 성공적으로 끝나야만 web의 빌드를 시작할 수 있습니다.
- 실행:
- (동시에 시작) packages/ui 빌드 & packages/utils 빌드
- (ui 빌드 완료 후 시작) apps/web 빌드
turborepo를 통한 초기 세팅 시 구조
/my-monorepo <-- 여기가 '워크스페이스' (하나의 집)
├── apps/ <-- '애플리케이션'이라는 방들이 있는 층 (워크스페이스의 일부)
│ ├── web/ <-- 'web'이라는 방 (워크스페이스의 멤버 1)
│ └── docs/ <-- 'docs'라는 방 (워크스페이스의 멤버 2)
├── packages/ <-- '공유 라이브러리'라는 방들이 있는 층 (워크스페이스의 일부)
│ ├── ui/ <-- 'ui'라는 방 (워크스페이스의 멤버 3)
│ └── utils/ <-- 'utils'라는 방 (워크스페이스의 멤버 4)
├── package.json
└── pnpm-workspace.yaml
추가로, 모노레포 프로젝트를 구축하기 위해 조사한 내용 중 Turborepo 외에도 Yarn Workspaces를 활용한 방법에 대해서도 간략히 정리
☁️ Yarn Workspace
- 장점:
- Yarn Berry와의 호환성이 뛰어납니다.
- 간단한 설정으로 모노레포 구성이 가능합니다.
- yarn Berry란: Yarn v2 이상을 yarn berry라고 한다.
⚙️ Yarn Berry 도입 및 최적화
Yarn Berry의 Plug'n'Play(PnP) 기능을 활용하여 다음과 같은 최적화를 이루었습니다.
기존 방식 (node_modules) vs PnP 방식
| 구조 | 모든 패키지를 하위 디렉토리로 복사 | zip 파일로 캐시에 저장 |
| 모듈 탐색 | Node.js가 node_modules 경로를 수동 탐색 | Yarn이 .pnp.cjs에서 정확한 경로 매핑 제공 |
| 속도 | 느림 (디스크 I/O 많음) | 빠름 (경로 탐색 불필요) |
| 용량 | 중복된 패키지로 무거워짐 | zip 공유로 용량 절약 |
| 버전 충돌 | 중첩된 node_modules 구조로 충돌 발생 가능 | Workspace 단위로 완전 분리 가능 |
- 의존성 탐색 속도 향상: 기존의 node_modules 구조에서는 의존성 탐색에 시간이 오래 걸렸지만, PnP를 통해 빠른 탐색 속도를 달성했습니다.
- 용량 절감: 의존성 파일 크기를 약 60% 줄였습니다.
- 빌드 시간 단축: 빌드 시간을 60초 이상 단축했습니다.
또한, git sparse-checkout을 활용하여 필요한 디렉토리만 체크아웃함으로써 저장소 용량 문제를 해결했습니다.
🚧 Yarn Berry 사용 시 고려사항
- PnP 미지원 패키지: 일부 패키지는 PnP를 지원하지 않아 node_modules를 여전히 사용해야 하는 경우가 있습니다.
- 의존성 관리의 번거로움: PnP 모드에서는 각 하위 프로젝트에 직접 의존성을 추가해야 합니다.
✅ 결론 및 추천
- Yarn Berry + Yarn Workspace: 의존성 관리와 빌드 최적화에 유리하며, PnP 모드를 활용할 수 있습니다.
- Turborepo: 캐싱 기능을 통해 빌드 속도를 향상시킬 수 있으며, CI/CD 환경에서 유용합니다.
- Turborepo는 PnP 모드를 완전히 지원하지 않지만, nodeLinker 설정을 통해 일부 기능을 활용할 수 있습니다.
'✏️ 공부기록 > React & Next.js' 카테고리의 다른 글
| React-Native 필수 태그 정리 (0) | 2025.12.06 |
|---|---|
| 모노레포 Next15v Pretendard(프리텐다드) 폰트 적용하기 (3) | 2025.06.17 |
| 아토믹 디자인 패턴(Atomic Design)과 컴포넌트 주도 개발(Component-Driven Development, CDD) (1) | 2025.06.09 |
| package.json 파일의 역할 및 구조 (0) | 2025.06.09 |
| ✏️ json-server 배포하기(with Render) (0) | 2024.09.05 |