본문으로 바로가기
개발

모노레포 완벽 가이드 — Turborepo·Nx·pnpm 실전 비교와 선택 기준

A
AlwaysCorp 엔지니어링팀· 플랫폼 엔지니어링
||11분 읽기
#모노레포#Turborepo#Nx#pnpm#workspace#빌드도구#생산성#플랫폼엔지니어링

모노레포를 둘러싼 오래된 오해들

구글의 단일 리포지터리

구글은 약 20억 줄 이상의 코드를 단일 리포지터리(Google3)에서 관리합니다(Potvin & Levenberg, Communications of the ACM, 2016). 페이스북은 Mercurial 기반의 모노레포를, 마이크로소프트는 Azure DevOps의 GVFS로 거대 저장소를 운영합니다. 대규모 조직이 모노레포를 고르는 이유는 단순합니다. 변경의 연쇄가 한 PR 안에서 끝나기 때문입니다.

반면 한국의 많은 조직에서 "모노레포"라는 말은 아직도 의심스럽게 들립니다. CI가 느려진다, 충돌이 많아진다, 레거시가 섞인다. 이런 우려는 대개 도구를 갖추지 않은 모노레포에서 발생하는 문제이지, 모노레포 자체의 한계는 아닙니다. 현대의 Turborepo·Nx는 그 대부분을 해소합니다.

저희 회사(AlwaysCorp)도 withu_apps라는 이름으로 Flutter 앱 11개와 공유 패키지 4개를 단일 저장소에서 운영하고 있습니다. "하나의 저장소"라는 선택을 한 뒤 가장 먼저 체감한 이득은 코드 재사용이 아니라, 브랜딩 톤이 바뀔 때 모든 앱의 테마를 한 PR로 맞출 수 있다는 것이었습니다. 아래 내용은 그 과정에서 쌓인 시행착오와 문헌의 표준 사례를 섞어 정리한 것입니다.

언제 모노레포를 선택하는가

  • 프론트엔드·백엔드·모바일이 같은 타입 정의를 공유할 때
  • 공용 UI 라이브러리를 여러 앱에서 사용할 때
  • 마이크로서비스가 많지만 일관된 릴리스가 필요할 때
  • 플랫폼 팀이 공통 설정(린터·포매터·테스트 러너)을 중앙에서 관리할 때

이 중 2개 이상 해당한다면 모노레포의 이익이 오버헤드를 초과합니다.

---

pnpm Workspaces, 가장 가벼운 진입점

순수 패키지 매니저의 힘

pnpm의 `workspaces` 기능은 복잡한 도구 없이 모노레포를 구성할 수 있는 가장 단순한 방법입니다.

```yaml # pnpm-workspace.yaml packages:

  • 'apps/*'
  • 'packages/*'

```

``` my-monorepo/ ├── apps/ │ ├── web/ (Next.js) │ └── mobile/ (React Native) ├── packages/ │ ├── ui/ (공용 컴포넌트) │ └── config/ (ESLint, TS 설정) └── package.json ```

`workspace:*` 프로토콜을 쓰면 내부 패키지를 로컬 심볼릭 링크로 참조할 수 있습니다.

```json // apps/web/package.json { "dependencies": { "@my/ui": "workspace:*" } } ```

장점

  • 의존성 저장 공간 1/3 — content-addressable store
  • 부모 권한 엄격 관리 — phantom dependency 방지
  • 다른 도구와의 충돌 없음 (Turborepo·Nx 모두 위에 얹을 수 있음)

한계

pnpm만으로는 빌드 캐싱이나 의존성 그래프 분석을 자동 처리하지 않습니다. 작은 팀에서는 단일 `pnpm -r build`로 충분하지만, 수십 개 패키지로 확장되면 도구가 필요해집니다.

---

Turborepo, Vercel의 빌드 최적화 도구

핵심 기능: 지능형 캐싱

Turborepo의 가장 강력한 기능은 incremental cache입니다. `turbo.json`으로 태스크를 정의하면, 입력 파일이 바뀌지 않은 패키지의 빌드는 0초 만에 건너뜁니다.

```json { "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/", "dist/"] }, "test": { "dependsOn": ["build"], "outputs": ["coverage/**"] }, "dev": { "cache": false, "persistent": true } } } ```

`^build`는 "이 패키지가 의존하는 다른 패키지의 build가 먼저 끝나야 함"을 의미하는 구문입니다.

원격 캐시(Remote Caching)

Turborepo의 차별점은 팀 공유 원격 캐시입니다. 동료가 메인 브랜치에서 빌드한 결과를 내 로컬 머신이 그대로 가져다 씁니다. Vercel에 무료로 호스팅되는 원격 캐시가 제공되고, 자체 호스팅도 가능합니다.

기업 사례: Vercel 내부에서는 원격 캐시 도입 후 평균 CI 시간이 60% 감소했다고 공개했습니다(Vercel Engineering Blog, 2023).

학습 곡선

Turborepo는 "추상화를 최소화"하는 철학입니다. `turbo run build`는 결국 각 패키지의 `package.json` 스크립트를 실행할 뿐이어서, 기존 프로젝트에 얹어도 규칙이 거의 바뀌지 않습니다. 작은 팀에서의 도입 마찰이 낮습니다.

---

Nx, 풀스택 개발 플랫폼

도구가 아닌 프레임워크

Nx는 Turborepo보다 훨씬 광범위한 기능을 제공합니다.

  • 프로젝트 생성 제너레이터 (React, Next.js, NestJS, Node, Express 등)
  • 의존성 그래프 시각화
  • 코드 소유권(CODEOWNERS) 자동화
  • 영향 분석(affected) — 변경된 파일에 영향받는 프로젝트만 빌드·테스트
  • 빌드 캐시 (Turborepo보다 오래된 구현)
  • 분산 태스크 실행(Nx Cloud DTE) — CI에서 태스크를 여러 에이전트로 자동 병렬화

```bash # 변경 영향을 받는 프로젝트만 빌드 nx affected:build --base=main ```

장점

  • 대규모 팀(50명 이상)에서 제공하는 구조와 제너레이터의 가치가 큼
  • 의존성 그래프 시각화는 기술 부채 관리에 강력
  • React·Next.js·Node·Angular를 한 Repo에서 "방식 통일"로 운영 가능

단점

  • 러닝 커브가 가팔라 작은 팀에는 과잉
  • 프레임워크에 가까운 규격을 강제 — 기존 프로젝트 이식이 Turborepo보다 복잡
  • Nx 특유의 `project.json`·Executor 개념이 추가 학습 비용

---

시나리오별 선택 기준

시나리오 A. 2~5명 팀, Next.js + 공용 UI

pnpm workspaces + Turborepo. 가장 빠르게 구성 가능, 학습 곡선 낮음.

시나리오 B. 10~20명 팀, 웹·서버·모바일 모두 TS

Turborepo 우선. 이미 만들어진 각 앱 구조를 그대로 옮기고 `turbo.json`만 추가. 필요 시 원격 캐시 활성화.

시나리오 C. 50명+ 팀, Angular·React·NestJS 혼합

Nx. 제너레이터·프로젝트 규격·영향 분석이 큰 이익을 냅니다. Nx Cloud DTE로 CI 분산 실행.

시나리오 D. 오픈소스 라이브러리 컬렉션 (여러 NPM 패키지)

pnpm + Changesets. 퍼블리시 자동화가 최우선이므로 Changesets의 변경 이력 관리가 핵심.

시나리오 E. 거대 엔터프라이즈, 내부 플랫폼 팀 존재

Nx + 자체 구축 도구. 또는 Bazel(구글식).

---

공통적으로 맞닥뜨리는 문제와 해법

문제 1. CI가 모든 걸 매번 빌드한다

해법: 영향 분석(affected) 또는 Turbo의 캐싱 키를 활용. 변경된 패키지와 그 의존자만 빌드합니다.

문제 2. TypeScript Project References

해법: 내부 패키지마다 `tsconfig.json`에서 `references`를 명시하고, 빌드 시 `tsc --build`를 사용합니다. 에디터에서 "Go to Definition"이 정확히 원본으로 이동합니다.

문제 3. 공용 ESLint·Prettier 설정

해법: `packages/config/eslint-config.js`를 만들고, 각 앱의 `.eslintrc.cjs`에서 `extends: "@my/config/eslint"`로 참조. 모든 앱의 규칙이 즉시 동기화됩니다.

문제 4. Docker 이미지 빌드

해법: 멀티스테이지 빌드 + `turbo prune`. `turbo prune --scope=web --docker`를 실행하면 해당 앱에 필요한 최소 의존성만 추려냅니다. 이미지 크기를 수백 MB 줄일 수 있습니다.

문제 5. 환경 변수와 비밀

해법: 모노레포 루트에 `.env.example`을 두되, 실제 `.env`는 각 앱이 자체 보유. Doppler·Vault 같은 중앙 비밀 관리 도구 권장.

---

마이그레이션 체크리스트

기존 멀티 리포지터리를 모노레포로 옮길 때의 순서.

| 단계 | 내용 | |---|---| | 1 | pnpm workspaces 구조 결정 (apps·packages) | | 2 | 각 기존 리포의 git history를 `git subtree`로 병합 | | 3 | 공용 설정(eslint·prettier·tsconfig) 추출 | | 4 | 공용 UI·유틸리티를 `packages/`로 분리 | | 5 | Turborepo 또는 Nx 도입, 파이프라인 정의 | | 6 | CI를 affected·cache 기반으로 재구성 | | 7 | 기존 리포 아카이브 (읽기 전용 전환) |

가장 흔한 실수는 1단계에서 패키지 경계를 너무 세밀하게 쪼개는 것입니다. 처음에는 크게 묶어 두고, 재사용 수요가 생기면 점진적으로 분리하는 편이 안전합니다.

---

남겨두고 싶은 말

모노레포를 성공시키는 것은 Turborepo도 Nx도 아닙니다. 팀 전체가 "하나의 코드베이스에 기여한다"는 공통 책임감을 가질 수 있느냐가 결정적입니다. 이 문화가 없으면 아무리 좋은 도구를 붙여도 결국 각 팀이 자기 디렉토리만 보는 "작은 멀티 리포"로 회귀합니다.

반대로 이 문화가 자리 잡으면, 도구 선택은 거의 어떤 것이든 괜찮습니다. 처음 Turborepo로 가볍게 시작했다가 이후 Nx로 옮기는 일이 그렇게 큰 부담이 아닐 만큼 생태계가 성숙해 있기도 하고요. 결국 중요한 건 첫 세팅의 완벽함이 아니라, 한 저장소 안에서 서로 영향을 주고받는 코드를 있는 그대로 마주할 용기에 가깝다고 느낍니다.

A

AlwaysCorp 엔지니어링팀

플랫폼 엔지니어링

얼웨이즈 블로그에서 유용한 정보와 인사이트를 공유합니다.