WebPiki
tutorial

모노레포 구축 가이드: Turborepo 실전 활용

모노레포의 장단점, Turborepo 핵심 기능, 실제 프로젝트에 적용하는 방법을 정리했다.

팀이 커지면서 프로젝트가 여러 개로 쪼개지면 자연스럽게 고민이 생긴다. 프론트엔드, 백엔드, 공통 라이브러리, 디자인 시스템 — 이걸 각각 별도 저장소로 관리할까, 아니면 하나의 저장소에 다 넣을까. 후자가 모노레포다.

모노레포란

하나의 Git 저장소에 여러 프로젝트(패키지)를 함께 관리하는 방식이다. 구글, 메타, 마이크로소프트 같은 빅테크가 내부적으로 쓰고 있다는 얘기로 유명해졌는데, 규모가 작은 팀에서도 유용한 패턴이다.

저장소 구조를 보면 감이 온다:

my-monorepo/
├── apps/
│   ├── web/          # Next.js 프론트엔드
│   ├── api/          # Express 백엔드
│   └── admin/        # 관리자 대시보드
├── packages/
│   ├── ui/           # 공유 UI 컴포넌트
│   ├── config/       # ESLint, TypeScript 설정
│   └── utils/        # 공유 유틸리티 함수
├── package.json
└── turbo.json

apps/에는 배포 가능한 애플리케이션, packages/에는 공유 라이브러리가 들어간다. 핵심은 이 모든 게 하나의 저장소에 있다는 것이다.

모노레포 vs 폴리레포

폴리레포(polyrepo)는 프로젝트별로 별도 Git 저장소를 쓰는 전통적인 방식이다. 둘 다 장단점이 명확하다.

모노레포의 장점

코드 공유가 쉽다. 공통 유틸리티나 타입 정의를 packages/utils에 넣어두면 모든 앱에서 바로 import 할 수 있다. 폴리레포에서는 이걸 npm 패키지로 배포하고, 각 저장소에서 설치하고, 버전 관리하고... 공유 코드 하나 수정할 때마다 이 사이클을 돌아야 한다.

원자적 변경(Atomic Changes). UI 라이브러리의 API를 바꾸면서 그걸 쓰는 앱의 코드도 같이 수정하는 걸 하나의 커밋, 하나의 PR로 처리할 수 있다. 폴리레포에서는 라이브러리 PR → 배포 → 앱 PR → 배포의 여러 단계를 거쳐야 한다.

일관된 설정. ESLint, TypeScript, Prettier 설정을 한곳에서 관리하니까 모든 프로젝트가 같은 코딩 컨벤션을 따른다.

의존성 관리 단순화. 프로젝트 전체에서 하나의 node_modules를 공유할 수 있다(호이스팅). React 버전이 프로젝트마다 다른 상황을 방지할 수 있다.

모노레포의 단점

저장소가 커진다. 클론, 체크아웃이 느려질 수 있다. git sparse checkout 같은 기법으로 완화할 수 있지만 추가 관리 포인트다.

CI/CD가 복잡해진다. 어떤 패키지가 변경됐을 때 어떤 앱을 다시 빌드해야 하는지 파악해야 한다. 이걸 자동화하는 게 Turborepo 같은 도구의 핵심 역할이다.

권한 관리가 어렵다. Git 저장소 단위로 접근 권한을 관리하는 조직에서는, 하나의 저장소에 모든 코드가 있으면 권한을 세밀하게 나누기 어렵다.

팀 간 충돌. 여러 팀이 같은 저장소를 쓰면 main 브랜치에 대한 머지 충돌이 잦아질 수 있다.

Turborepo — 모노레포를 쓸 만하게 만드는 도구

Turborepo는 Vercel에서 관리하는 모노레포 빌드 시스템이다. 모노레포의 단점(느린 빌드, 복잡한 태스크 관리)을 해결하는 데 초점을 맞추고 있다. Rust로 작성돼서 빠르다.

핵심 기능 1 — 캐싱

Turborepo의 가장 큰 셀링 포인트. 각 태스크의 입력(소스 코드, 설정 파일, 환경 변수 등)을 해싱해서, 입력이 변하지 않았으면 이전 결과를 재사용한다.

# 처음 실행 — 전체 빌드
npx turbo build
# cache miss → 모든 패키지 빌드 실행

# 코드 변경 없이 다시 실행
npx turbo build
# cache hit → 즉시 완료. 빌드를 아예 건너뜀

로컬 캐시뿐 아니라 리모트 캐싱도 지원한다. CI에서 빌드한 결과를 팀원 전체가 공유할 수 있다. 한 명이 빌드해놓으면 다른 팀원은 같은 코드에 대해 빌드를 다시 할 필요가 없다. Vercel에 계정이 있으면 리모트 캐시를 바로 쓸 수 있고, 자체 서버에 캐시를 호스팅할 수도 있다.

핵심 기능 2 — 병렬 실행

의존 관계가 없는 태스크는 자동으로 병렬 실행한다. 예를 들어 web, api, admin 세 앱의 빌드가 서로 독립적이면 동시에 돌린다.

   ┌─── web (build) ──┐
   │                   │
   ├─── api (build) ──┤──→ 전체 완료
   │                   │
   └── admin (build) ──┘

코어 수에 따라 병렬도가 결정되고, 의존 관계가 있는 태스크는 순서를 지켜서 실행한다.

핵심 기능 3 — 태스크 파이프라인

turbo.json에서 태스크 간의 의존 관계를 정의한다.

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

"dependsOn": ["^build"]에서 ^는 "이 패키지가 의존하는 다른 패키지의 build를 먼저 실행하라"는 뜻이다. webui를 의존하고 있으면, ui의 build가 끝난 다음 web의 build가 실행된다.

outputs는 캐싱 대상을 지정한다. dist/**를 캐싱해놓으면 다음에 같은 입력으로 빌드할 때 이 폴더를 복원해서 빌드를 건너뛴다.

Nx와 비교

모노레포 도구의 양대산맥이 Turborepo와 Nx다.

TurborepoNx
개발사VercelNrwl (독립)
언어RustTypeScript + Rust (일부)
설정 복잡도낮음중간~높음
기능 범위빌드 시스템에 집중빌드 + 코드 생성 + 의존성 그래프 시각화 + 플러그인 등 종합
캐싱로컬 + 리모트로컬 + Nx Cloud
프레임워크 통합프레임워크 무관React, Angular, Node 등 공식 플러그인
마이그레이션기존 프로젝트에 쉽게 추가프로젝트 구조 변경 필요할 수 있음

Turborepo는 설정이 간단하고 "빌드를 빠르게 하는 것"에 집중한다. 기존 프로젝트에 turbo.json 하나 추가하면 바로 쓸 수 있다. Vercel과의 통합이 자연스러운 것도 장점.

Nx는 기능이 훨씬 많다. 코드 제너레이터, 의존성 그래프 시각화, 영향 분석(affected commands) 등 모노레포 관리에 필요한 거의 모든 기능을 제공한다. 대신 학습 곡선이 있고, Nx 방식에 맞춰 프로젝트를 구성해야 하는 부분이 있다.

작은~중간 규모 프로젝트에서 빠르게 시작하고 싶다면 Turborepo. 대규모 팀에서 체계적인 모노레포 관리가 필요하다면 Nx가 더 나을 수 있다.

실전 — Turborepo 프로젝트 셋업

처음부터 시작하는 경우:

npx create-turbo@latest my-monorepo
cd my-monorepo

이미 있는 프로젝트에 추가하는 경우:

npm install turbo --save-dev

package.json 설정

루트 package.json에 workspaces를 정의한다:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "test": "turbo test"
  }
}

"private": true는 루트 패키지가 npm에 배포되지 않도록 하는 설정이다. 모노레포 루트는 항상 이렇게 설정한다.

패키지 간 의존 관계 설정

apps/web에서 packages/ui를 쓰고 싶다면:

// apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "ui": "workspace:*"
  }
}

workspace:*는 npm/pnpm/yarn이 이 의존성을 모노레포 내부의 ui 패키지로 해석하게 한다. npm에서 다운로드하는 게 아니라 로컬 패키지를 참조하는 것이다.

CI/CD 통합

GitHub Actions에서의 예시:

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx turbo build lint test

Turborepo의 캐싱 덕분에, 변경되지 않은 패키지는 빌드를 건너뛰니까 CI 시간이 크게 줄어든다. 리모트 캐싱을 설정하면 더 효과적이다.

Vercel에 배포하는 경우, Turborepo를 자동으로 인식해서 변경된 앱만 재배포한다. 이게 Vercel + Turborepo 조합의 가장 큰 장점 중 하나다.

모노레포를 쓰지 않아야 할 때

모노레포가 만능은 아니다. 오히려 안 쓰는 게 나은 경우도 있다.

프로젝트 간 관련성이 없을 때. 완전히 독립적인 프로젝트를 하나의 저장소에 넣을 이유는 없다. 코드 공유도 없고, 같이 배포할 일도 없으면 폴리레포가 낫다.

팀 규모가 아주 클 때 (수백 명 이상). 표준적인 Git 도구로는 거대한 모노레포를 감당하기 어렵다. 구글이나 메타 수준이면 독자적인 VCS(Version Control System) 도구를 만들어 쓴다. 일반적인 팀이 따라 할 수 있는 접근이 아니다.

보안/접근 제어가 중요할 때. 프로젝트별로 접근 권한을 엄격하게 분리해야 한다면 모노레포는 불편하다. GitHub의 CODEOWNERS 같은 걸로 부분적으로 제어할 수 있지만, 저장소 자체를 분리하는 것만큼 확실하지 않다.

기술 스택이 완전히 다를 때. Go 백엔드와 React 프론트엔드가 빌드 시스템, 패키지 매니저, 의존성 관리 방식이 전부 다르다면, 하나의 저장소에 넣어도 시너지가 적다.

시작하는 법

모노레포에 관심이 있다면, 작게 시작하는 걸 권한다. 기존 프로젝트에서 공유 유틸리티나 타입 정의 패키지 하나만 분리해보는 거다.

  1. packages/shared/를 만들고
  2. 공통 타입이나 유틸 함수를 옮기고
  3. turbo.json을 추가하고
  4. 빌드가 잘 되는지 확인한다

이 정도면 모노레포의 기본 개념과 Turborepo의 동작 방식을 파악하기에 충분하다. 효과가 있다 싶으면 점진적으로 확장하면 된다. 처음부터 완벽한 모노레포 구조를 설계하려고 하면 오히려 시작 자체가 늦어진다.

#모노레포#Turborepo#Nx#웹개발#DevOps

관련 글