8개 멀티레포를 Turborepo 모노레포로 합친 스타트업의 6개월 — 빌드 시간 72% 단축 기록
4년차 SaaS 스타트업이 8개 멀티레포를 Turborepo 기반 모노레포로 전환한 실제 과정. Lerna 실패 이유, Turborepo 도입 결정, 전환 중 막힌 지점 3가지, 6개월 후 CI 빌드 72% 단축 결과까지.
8개의 분리된 레포지토리를 운영하던 한 SaaS 스타트업 개발팀이 Turborepo 기반 모노레포로 전환한 뒤 CI 빌드 시간을 72% 줄이고, 공유 컴포넌트 배포 사이클을 8개 PR에서 1개 PR로 압축했다. 이 글은 그 팀이 6개월 동안 겪은 시행착오와 실제 수치를 3인칭으로 정리한 케이스 스터디다.
멀티레포를 운영 중이고 빌드 속도 문제나 패키지 의존성 충돌로 고통받고 있다면, 또는 모노레포 전환을 고민하고 있다면 이 팀의 경험이 판단 기준이 될 수 있다.
이 팀이 마주한 문제 — 8개 레포, 하나의 코드베이스
이 스타트업은 창업 4년차에 접어든 B2B SaaS 팀이다. 제품은 하나지만 코드베이스는 여러 해에 걸쳐 8개의 독립 레포지토리로 쪼개졌다. 프론트엔드 앱 3개(고객용 웹, 관리자 대시보드, 내부 운영 툴), 백엔드 서비스 3개(API 서버, 인증 서비스, 알림 서버), 그리고 공유 UI 컴포넌트 패키지와 공유 유틸리티 패키지 레포 2개로 구성된 구조였다.
팀 규모는 개발자 12명. 각 레포마다 CI 파이프라인이 독립적으로 운영되고 있었고, 코드 리뷰 채널도 사실상 8개가 병렬로 돌아가고 있었다.
핵심 문제는 공유 패키지였다. 예를 들어 공유 UI 컴포넌트의 버튼 스타일 하나를 수정하면, 이를 사용하는 3개 프론트엔드 앱 각각에서 패키지 버전을 올린 PR을 따로 생성하고, 별도로 리뷰받고, 별도로 배포해야 했다. 단순 색상 변경 하나에 최소 4개의 PR이 필요했고, 팀원들은 "버튼 색 바꾸는 데 하루가 걸린다"는 말을 반복했다고 한다.
이 팀의 시니어 엔지니어는 당시 상황을 이렇게 표현했다. "코드는 하나의 제품인데, 저장소는 8개 회사처럼 운영되고 있었다."
전환 전: 8개의 독립 레포지토리가 각각의 CI 파이프라인을 운영하던 구조
팀이 특히 고통받은 지점은 두 가지였다. 첫째, 의존성 버전 드리프트. 공유 유틸리티 패키지가 세 가지 다른 버전으로 서로 다른 앱에서 사용되다 보니, 버그가 어느 버전에서 발생했는지 추적하는 데만 수 시간이 소요됐다. 둘째, CI 비용. 8개 파이프라인이 각각 전체 빌드를 수행하면서 월 CI 시간이 800분을 넘겼다.
시도 1: Lerna로 해결하려 했으나 실패한 이유
팀은 2023년 초, 첫 번째 해결책으로 Lerna v3를 도입했다. 당시 팀 내에서 Lerna에 대한 경험이 있는 엔지니어가 있었고, 모노레포 전환 사례 자료도 Lerna 기반이 많았다. 마이그레이션은 약 3주 만에 완료됐고 팀은 기대감이 높았다.
그러나 문제는 곧 드러났다. Lerna의 의존성 호이스팅이 일부 패키지와 충돌을 일으켰다. 특히 두 개의 프론트엔드 앱이 동일 패키지의 서로 다른 버전을 필요로 하는 경우, 호이스팅이 예상과 다르게 작동하면서 런타임 에러가 발생했다.
주의: Lerna는 여전히 유효한 도구다. 그러나 빌드 캐시 기능 없이는 CI 속도 문제를 근본적으로 해결할 수 없다. 이 팀의 경우, Lerna 도입 후 오히려 CI 빌드 시간이 평균 22분으로 늘어났다 — 모든 패키지를 순차적으로 빌드하는 구조 때문이었다.
더 큰 문제는 버전 관리였다. Lerna의 fixed 모드는 모든 패키지 버전을 동일하게 올려버렸고, independent 모드는 각 패키지 버전을 수동으로 관리해야 해서 12명의 팀에서 일관성을 유지하기 어려웠다. 결국 이 팀은 Lerna 도입 3개월 만에 재검토에 들어갔고, Turborepo로 방향을 바꿨다.
Turborepo 도입 결정 — 무엇이 달랐나
팀이 Turborepo를 선택한 이유는 세 가지로 정리된다.
빌드 캐시: 변경되지 않은 패키지는 캐시된 결과를 재사용한다. Turborepo 공식 벤치마크 기준, 캐시 히트 시 빌드 시간을 최대 90%까지 줄일 수 있다.
파이프라인 병렬화: 의존성 그래프를 분석해 독립적인 태스크를 병렬로 실행한다. Lerna의 순차 빌드와 달리 실제 의존 관계가 없는 패키지들은 동시에 빌드된다.
pnpm workspaces 통합: 기존에 npm을 사용하던 일부 레포를 pnpm으로 통일하면서 의존성 호이스팅 문제를 pnpm의 엄격한 격리 모드로 해결할 수 있었다.
팀은 6주짜리 마이그레이션 로드맵을 세웠다. 1~2주차에 레포 통합 및 pnpm-workspace.yaml 구성, 3~4주차에 turbo.json 파이프라인 설정 및 로컬 빌드 검증, 5~6주차에 CI 파이프라인 전환 및 Vercel Remote Cache 연결. 실제로는 7주가 걸렸다고 한다.
monorepo/
├── apps/
│ ├── web/ # 고객용 Next.js 앱
│ ├── admin/ # 관리자 Next.js 앱
│ └── api/ # Express API 서버
├── packages/
│ ├── ui/ # 공유 UI 컴포넌트
│ ├── config/ # 공유 설정 (ESLint, TypeScript)
│ └── utils/ # 공유 유틸 함수
├── turbo.json
└── pnpm-workspace.yaml
Turborepo의 태스크 그래프: 의존 관계가 없는 패키지는 병렬로 빌드된다
전환 과정 — 막혔던 지점 3가지
전환이 순조롭지만은 않았다. 팀이 실제로 막혔던 지점 세 가지는 다음과 같다.
문제 1 — Next.js App Router와 shared packages 타입 충돌: 공유 UI 패키지가 React.FC 타입을 export할 때 Next.js App Router 환경에서 use client 지시어 없이 사용하면 TypeScript 타입 에러가 발생했다. 해결책은 패키지 진입점에 "use client"를 명시하거나, Server Component와 Client Component용 진입점을 분리하는 것이었다. 이 팀은 packages/ui/src/server.ts와 packages/ui/src/client.ts로 진입점을 나누는 방식을 택했다.
문제 2 — CI에서 원격 캐시 설정: 로컬에서는 캐시가 잘 작동했지만, CI 환경(GitHub Actions)에서는 캐시가 무효화됐다. Vercel Remote Cache를 사용하려면 TURBO_TOKEN과 TURBO_TEAM 환경변수를 GitHub Secrets에 등록해야 한다. 이 설정 없이는 CI 빌드마다 전체 캐시가 초기화되어 캐시 효과가 없었다. Self-hosted 원격 캐시 대안으로는 turborepo-remote-cache 오픈소스가 있다.
문제 3 — 팀원 로컬 환경 불일치: Node.js 버전이 팀원마다 달랐다(v18~v22 혼재). 특정 패키지가 Node.js 버전에 따라 빌드 결과가 달라지는 현상이 발생했다. 이 팀은 루트에 .nvmrc 파일을 추가하고 CI에서도 동일 버전을 강제했다. pnpm 버전 통일에는 package.json의 engines 필드와 packageManager 필드를 함께 사용했다.