Ray Book
프론트엔드 인프라

모노레포

왜 하나의 저장소에 여러 패키지를 넣는가, Turborepo, Nx의 태스크 캐싱과 의존성 그래프를 시각화합니다

inframonorepoturboreponxworkspaces

멀티레포의 고통

대부분의 프로젝트는 하나의 저장소 (repository) 에서 시작합니다. 그런데 프로젝트가 성장하면서 관리자 페이지, 디자인 시스템, 공통 유틸리티 같은 별도 프로젝트가 생기기 시작합니다.

각각을 독립된 저장소로 관리하는 것이 멀티레포입니다.

github.com/team/web-app        ← 사용자 앱
github.com/team/admin-app      ← 관리자 앱
github.com/team/shared-ui      ← 공통 컴포넌트 (npm 배포)
github.com/team/shared-utils   ← 공통 유틸리티 (npm 배포)

처음엔 깔끔해 보이지만, 시간이 지나면 다음 문제들이 쌓입니다.

코드 중복

formatDate, cn() 같은 유틸리티가 여러 저장소에 복사됩니다. shared 패키지로 추출해도, 변경할 때마다 npm에 배포하고 → 각 저장소에서 업데이트하는 루프가 필요합니다.

버전 드리프트

web-app은 React 18, admin-app은 React 19를 쓰는 상황이 발생합니다. 공통 컴포넌트가 둘 다 지원해야 하는 부담이 생깁니다.

CI/CD 파편화

저장소마다 GitHub Actions 워크플로우를 따로 관리합니다. eslint 설정, tsconfig, 배포 스크립트가 조금씩 다릅니다. 하나를 고치면 나머지도 고쳐야 하는데, 잊기 쉽습니다.

원자적 변경의 어려움

shared-ui의 API를 변경하면, web-app과 admin-app도 함께 수정해야 합니다. 멀티레포에서는 3개의 PR을 각각 만들고, 배포 순서를 조율해야 합니다.

모노레포의 장점

모노레포는 여러 프로젝트를 하나의 Git 저장소에 넣는 구조입니다.

monorepo/
  apps/
      web/           ← 사용자 앱
      admin/         ← 관리자 앱
  packages/
      ui/            ← 공통 컴포넌트
      utils/         ← 공통 유틸리티
      tsconfig/      ← 공유 설정
  package.json       ← 루트
  turbo.json         ← Turborepo 설정

멀티레포의 고통이 해소됩니다.

문제멀티레포모노레포
코드 공유npm 배포 필요로컬 링크 (workspace)
버전 통일수동 관리루트에서 일괄 관리
CI/CD저장소별 파편화단일 파이프라인
원자적 변경PR 3개 + 배포 조율PR 1개로 모든 패키지 수정

Workspaces

모노레포의 핵심은 패키지 매니저의 workspace 기능입니다. 패키지 간 의존성을 로컬 파일시스템에서 직접 링크합니다.

npm workspaces

// 루트 package.json
{
  "workspaces": ["apps/*", "packages/*"]
}

pnpm workspaces

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

Yarn workspaces

// 루트 package.json
{
  "workspaces": ["apps/*", "packages/*"]
}

workspace를 설정하면, 패키지에서 다른 패키지를 의존성으로 선언할 수 있습니다.

// apps/web/package.json
{
  "dependencies": {
    "@mono/ui": "workspace:*",
    "@mono/utils": "workspace:*"
  }
}

workspace:*는 npm 레지스트리 대신 로컬 패키지를 직접 링크합니다. @mono/ui를 수정하면 web에서 즉시 반영됩니다.

Turborepo, 태스크 캐싱과 병렬 실행

workspace만으로는 모노레포를 효율적으로 운영하기 어렵습니다. 패키지가 30개인 모노레포에서 npm run build를 하면, 변경 없는 패키지까지 전부 빌드합니다.

Turborepo는 이 문제를 해결하는 빌드 시스템입니다.

태스크 그래프

Turborepo는 turbo.json을 분석하여 패키지 간 의존 관계를 파악하고, 태스크 그래프를 만듭니다.

// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {}
  }
}

"dependsOn": ["^build"]에서 ^의존하는 패키지의 build가 먼저 실행되어야 한다는 뜻입니다.

turbo run build 실행 시

  1단계  @mono/utils:build, @mono/tsconfig:build   (병렬)
  2단계  @mono/ui:build                            (1단계 결과에 의존)
  3단계  apps/web:build, apps/admin:build          (병렬)

독립적인 태스크는 자동으로 병렬 실행됩니다.

캐싱 메커니즘

Turborepo는 각 태스크에 대해 fingerprint (해시)를 계산합니다. 입력이 같으면 결과도 같다는 원리입니다.

fingerprint에 포함되는 요소:

  • 소스 파일의 내용
  • 의존성 버전
  • 환경 변수
  • turbo.json 설정
$ turbo run build

# @mono/utils:build, cache hit, replaying output  ← 0.1초
# @mono/ui:build, cache hit, replaying output     ← 0.1초
# apps/web:build, cache miss, executing           ← 12초
# apps/admin:build, cache hit, replaying output   ← 0.1초

캐시 히트 시, 이전 빌드 결과 (dist/ 폴더)를 그대로 복원합니다. 로그 출력까지 재생합니다.

리모트 캐시

로컬 캐시만으로는 CI 환경에서 의미가 없습니다. 매번 새 머신에서 실행하니까요. Turborepo는 리모트 캐시를 지원합니다.

# Vercel Remote Cache 연결
npx turbo login
npx turbo link

팀원 A가 빌드한 결과를 팀원 B와 CI 서버가 공유합니다. 한 사례에서는 리모트 캐시 도입으로 Turbo 태스크 시간이 약 50% 감소했습니다.

Nx, Affected와 프로젝트 그래프

Nx는 Turborepo와 비슷한 목표를 가진 빌드 시스템이지만, 몇 가지 차별점이 있습니다.

Affected 명령어

Nx의 가장 강력한 기능은 nx affected입니다. Git diff와 프로젝트 그래프를 결합하여 변경된 패키지와 그에 의존하는 패키지만 작업을 실행합니다.

# shared를 수정한 후
nx affected --target=build

# ✓ shared:build        ← 변경됨
# ✓ web:build           ← shared에 의존 → 영향받음
# ✓ admin:build         ← shared에 의존 → 영향받음
# - docs:build          ← shared에 의존하지 않음 → 스킵

프로세스는 다음과 같습니다.

  1. Git으로 변경된 파일을 감지
  2. 프로젝트 그래프에서 해당 파일이 속한 패키지를 찾음
  3. 그 패키지에 의존하는 모든 패키지를 추적
  4. 추적된 패키지만 태스크 실행

Nx vs Turborepo

특성TurborepoNx
설정 복잡도낮음 (turbo.json 하나)높음 (프로젝트별 설정)
캐싱로컬 + Vercel 리모트로컬 + Nx Cloud 리모트
Affected없음 (전체 빌드 + 캐시)Git diff 기반 영향 분석
코드 생성없음풍부한 generator 지원
플러그인최소React, Angular, Next.js 등
학습 곡선낮음중간~높음

Turborepo는 "기존 모노레포에 캐싱을 얹는" 가벼운 접근이고, Nx는 "모노레포 전체를 관리하는" 포괄적 접근입니다.

모노레포의 단점

모노레포가 만능은 아닙니다.

  • 저장소 크기, 코드가 한 곳에 모이므로 Git 저장소가 커집니다. git clone이 느려질 수 있습니다 (shallow clone으로 완화)
  • CI 복잡도, 변경 감지, 선택적 빌드, 캐싱 설정이 필요합니다. Turborepo/Nx 없이는 모든 패키지를 매번 빌드하게 됩니다
  • 권한 관리, GitHub은 저장소 단위로 권한을 설정합니다. 모노레포에서는 CODEOWNERS 파일로 디렉토리별 리뷰어를 지정해야 합니다
  • 의존성 충돌, 패키지 A는 React 18, 패키지 B는 React 19를 원하는 경우. 모노레포에서는 버전을 통일하거나, pnpm의 overrides로 관리해야 합니다

시각화

멀티레포에서 모노레포로, 그리고 Turborepo와 Nx의 최적화 전략을 단계별로 봅니다.

Step 1/4멀티레포 — 각각의 저장소

프로젝트마다 별도 저장소를 운영합니다. 독립적이지만, 공통 코드가 중복되고 의존성 버전이 서로 달라지는 버전 드리프트가 발생합니다.

repo-webpackage.json · CI/CD · utils/format.ts
repo-adminpackage.json · CI/CD · utils/format.ts
repo-sharedpackage.json · CI/CD · npm publish
코드 중복버전 드리프트CI/CD 파편화

체크리스트

  • 2개 이상의 프로젝트가 코드를 공유하고 있다면, 모노레포를 검토했는가?
  • workspace 설정으로 패키지 간 로컬 링크가 동작하는가?
  • Turborepo나 Nx로 태스크 캐싱을 설정했는가?
  • CI에서 리모트 캐시를 활용하고 있는가?
  • CODEOWNERS로 디렉토리별 리뷰어를 지정했는가?
  • 의존성 버전이 패키지 간에 일관적인가?