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 실행 시:

  @mono/utils:build   @mono/tsconfig:build
        ↓                    ↓
  @mono/ui:build  ←┘

  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로 디렉토리별 리뷰어를 지정했는가?
  • 의존성 버전이 패키지 간에 일관적인가?