Turborepo 심화 가이드 — 원격 캐시·파이프라인·증분 빌드·대규모 모노레포
이 글의 핵심
이 글은 기본 Turborepo 사용법을 넘어, 원격 캐시 전략·태스크 그래프 튜닝·증분 빌드·여러 프레임워크 공존·컨테이너와 파이프라인·Changesets 릴리스까지 “규모가 커진 뒤”에 필요한 패턴을 한데 모았습니다. 대규모 모노레포에서 빌드 시간·비용·팀 협업을 동시에 잡는 관점으로 읽으면 좋습니다.
이 글을 읽으면 (읽는 시간: 약 60분)
TL;DR: Turborepo는 “빠른 로컬 빌드”를 넘어, 원격 캐시로 팀·CI가 같은 산출물을 재사용하고, 파이프라인(태스크 그래프)을 정확히 정의해 증분 빌드의 이득을 극대화할 때 진가가 드러납니다. 여기에 다중 프레임워크, Docker 레이어 캐시, CI 매트릭스, Changesets 기반 릴리스까지 엮으면 수십~수백 패키지 규모에서도 운영 가능한 모노레포가 됩니다.
이 글을 읽으면:
- 원격 캐시를 도입·운영할 때 필요한 전략(보안, 키 관리, 캐시 무효화)을 이해할 수 있습니다.
turbo.json수준에서 파이프라인을 최적화하고, 증분 빌드가 깨지는 대표 원인을 진단할 수 있습니다.- Next.js·Vite·Nest 등 이질적인 앱이 한 저장소에 있을 때의 경계와 공통 패키지 설계를 정리할 수 있습니다.
- Docker·CI/CD에서 캐시를 살리는 패턴과 Changesets와의 연동을 실무 관점으로 설계할 수 있습니다.
전제 지식: 워크스페이스(pnpm·yarn·npm), 기본 turbo run 경험. 입문은 Turborepo 완벽 가이드를 먼저 보는 것을 권합니다.
1. 왜 “심화”가 별도 주제인가
모노레포가 커지면 문제의 중심은 코드 양이 아니라 빌드 그래프의 불확실성으로 이동합니다. 로컬에서는 빠른데 CI만 느리거나, 특정 브랜치에서만 캐시가 전부 미스 나거나, 한 패키지의 린트 설정 변경이 전 앱의 빌드를 연쇄적으로 무효화하는 식입니다. Turborepo의 심화 과제는 크게 세 가지입니다.
- 캐시 키가 의도대로 좁혀지는가 (입력 해시·산출물·환경 변수)
- 태스크 의존성이 실제 코드 의존성과 일치하는가 (
dependsOn,topological) - 원격 캐시를 팀·파이프라인이 안전하게 공유하는가 (토큰, 리전, 정책)
이 세 가지가 맞물려야 “증분 빌드”가 이론이 아니라 비용 절감으로 이어집니다.
2. Remote Caching 전략
2.1 원격 캐시가 해결하는 문제
로컬 디스크 캐시는 같은 머신에서만 유효합니다. 팀원 A가 이미 빌드한 산출물을 팀원 B와 CI가 재사용하지 못하면, 모노레포의 병렬성은 “사람마다 한 번씩” 전체에 가깝게 비용이 늘어납니다. 원격 캐시는 동일한 입력 해시에 대해 저장소에 아티팩트 메타데이터를 올려두고, 다른 환경이 다운로드·복원할 수 있게 합니다.
2.2 Vercel Remote Cache와 팀 운영
Vercel에 연동된 프로젝트에서는 원격 캐시 연동이 단순해지는 경우가 많습니다. 실무에서는 다음을 명시적으로 정합니다.
- 어느 환경이 캐시를 읽고 쓸 수 있는지(예:
main은 읽기+쓰기, PR은 읽기 위주) - 시크릿 이름과 로테이션 주기(CI 변수, OIDC 연동 여부)
- 캐시 미스가 기본인 작업(네이티브 모듈, 플랫폼 의존 빌드)에 대한 예외 정책
원격 캐시는 “빌드 결과를 공유”하는 장치이므로, 민감한 산출물이 캐시에 올라가면 안 되는지(환경 변수 파일, 서명 키, 내부 전용 바이너리)를 보안 검토와 함께 두는 것이 좋습니다.
2.3 자체 호스팅·S3 호환 스토리지
규제·데이터 레지던시·비용 구조 때문에 클라우드 기본 제공만으로는 부족한 조직이 있습니다. 이때는 S3 호환 API를 쓰는 스토리지에 원격 캐시를 두고, 버킷 정책·VPC 엔드포인트·감사 로그를 인프라 표준에 맞춥니다. 핵심은 “캐시 버킷 = 빌드 산출물”이라는 점에서 객체 수명 주기 정책(예: 30~90일 후 만료)과 비용 알림을 함께 두는 것입니다.
2.4 캐시 적중률을 깎는 대표 실수
- 환경 변수 남발: 빌드 스크립트가 매번 다른 env를 주입하면 해시가 흔들립니다. CI에서는
COMMIT_SHA처럼 불가피한 값만 노출하고, 나머지는.env파일이 아닌 CI 시크릿 스토어에서 런타임에 주입하는 편이 낫습니다. - 시간 의존 코드:
new Date()를 빌드 산출물에 박아 넣으면 캐시가 사실상 무용지물이 됩니다. 필요하면 빌드 ID로 대체합니다. - 플랫폼 불일치: Linux CI와 macOS 로컬의 네이티브 바이너리가 다르면 캐시 공유가 어렵습니다. 이때는 해당 태스크를 캐시 대상에서 제외하거나, 크로스 컴파일 전략을 따로 둡니다.
3. Pipeline 최적화 (turbo.json)
3.1 태스크 그래프를 “의도”로 만든다
turbo.json의 pipeline(또는 최신 설정에서는 tasks 키를 사용하는 형태로 통합되는 추세입니다. 문서·버전에 따라 키 이름이 다를 수 있으므로, 사용 중인 Turborepo 메이저 버전에 맞는 공식 문서를 기준으로 맞춥니다)은 단순한 설정 파일이 아니라 팀의 빌드 의존 관계를 코드화한 계약입니다. build가 ^build에 의존한다는 것은 “패키지 간 빌드 순서를 위상 정렬하라”는 뜻이며, 루트 스크립트만으로는 표현하기 어렵습니다.
3.2 dependsOn 패턴
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"lint": {
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "build/**"]
}
}
}
위 예에서 test가 ^build에 의존하면, 의존 패키지가 먼저 빌드된 뒤 테스트가 실행됩니다. 모노레포에서 흔한 실수는 test가 로컬 소스만 보면 된다고 생각하고 dependsOn을 비워 두는 것입니다. 그러나 워크스페이스 패키지가 트랜스파일된 dist를 참조하는 구조라면, 테스트가 옛 산출물을 보고 통과하는 거짓 양성이 생깁니다.
3.3 outputs와 다운스트림 무효화
outputs는 “이 태스크가 생산한 아티팩트”를 Turborepo에 알려 캐시 복원과 후속 태스크 연결에 쓰입니다. outputs가 비어 있거나 범위가 넓으면, 캐시는 동작해 보여도 다음 단계가 매번 재실행될 수 있습니다. 반대로 너무 좁으면 필요한 파일이 누락되어 런타임 오류가 납니다.
프레임워크별 산출 디렉터리(.next, dist, build 등)를 팀 표준으로 고정하고, 각 패키지의 package.json files 필드와 Turbo outputs가 서로 모순되지 않게 맞추는 것이 좋습니다.
3.4 persistent·long-running 태스크
개발 서버·스토리북·E2E 워처처럼 종료되지 않는 프로세스는 persistent: true로 표시하는 패턴이 있습니다. 이런 태스크를 일반 build와 동일한 파이프라인에 섞으면 병렬성·캐시 전략이 꼬이기 쉬우므로, 보통 dev 전용 파이프라인으로 분리하고 CI 기본 파이프라인에서는 제외합니다.
4. Incremental Builds(증분 빌드)의 실체
4.1 “변경된 것만”의 조건
Turborepo는 파일 시스템 스냅샷과 설정을 기반으로 입력 해시를 계산합니다. 증분 빌드가 성립하려면 다음이 필요합니다.
- 소스 파일이 의미 있는 경로에 있다 (빌드 도구가 읽는 트리 밖에 두면 매번 미스).
- 설정 파일(
tsconfig, 번들러 설정)이 캐시 입력에 포함된다. - 생성 코드·프로토콜 버퍼 등 코드젠이 있다면, 생성 스크립트와 입력 스키마가 파이프라인에 묶인다.
4.2 필터링으로 범위 줄이기
로컬에서는 다음과 같은 패턴으로 작업 범위를 줄입니다.
# 예: 특정 패키지와 그 의존성만
pnpm turbo run build --filter=web-app...
# 예: 변경된 패키지 기준 (도구·버전에 따라 옵션 이름이 다를 수 있음)
pnpm turbo run test --filter=[HEAD^1]
핵심은 “필터는 실행 범위를 줄이고, 캐시는 입력 해시가 맞을 때 복원한다”는 점입니다. 필터를 써도 입력이 동일하면 원격 캐시에서 가져올 수 있고, 반대로 필터를 써도 입력이 흔들리면 매번 미스가 납니다.
4.3 대규모 저장소에서의 “그래프 위생”
증분 빌드가 깨지는 흔한 원인은 숨은 의존성입니다. 예를 들어 apps/web이 packages/ui를 공식 의존성으로 두지 않고 상대 경로로 import하면, 도구는 소스 레벨에서만 “우연히” 동작합니다. 이런 구조는 Turborepo뿐 아니라 릴리스·버전 정책 전체를 부패시킵니다. 해결책은 워크스페이스 의존성을 package.json에 명시하고, ESLint 등으로 금지 패턴을 잡는 것입니다.
5. 다중 프레임워크 통합
5.1 한 모노레포에 Next·Vite·Nest가 공존할 때
이질적인 런타임이 공존하면 빌드 도구·환경 변수·테스트 러너가 달라집니다. 패턴은 다음과 같이 정리할 수 있습니다.
apps/*: 배포 단위(프론트·백오피스·API). 각 앱은 자체 번들러·런타임을 가진다.packages/*: 공유 UI·유틸·타입·클라이언트 SDK. 프레임워크에 묶이지 않은 순수 TS를 최대화한다.- 경계: React Server Components 규칙, Node 전용 API, 브라우저 전용 API가 섞이지 않게 엔트리 포인트를 나눈다.
5.2 공유 UI와 트리셰이킹
여러 앱이 @acme/ui를 쓸 때, 빌드 산출물이 번들 전체로 퍼지면 성능 이슈로 돌아옵니다. package.json의 sideEffects, exports 필드, 그리고 번들러별 설정을 팀 표준으로 맞추고, Turbo의 build 산출물이 ESM/CJS 혼선을 일으키지 않게 합니다.
5.3 API 계약과 타입 공유
프론트와 백이 분리되어 있어도, OpenAPI·tRPC·GraphQL 스키마에서 생성한 타입을 packages/contracts로 두면 breaking change를 빌드 타임에 잡을 수 있습니다. 이때 contracts 패키지의 빌드·코드젠 태스크를 파이프라인 상에서 ^build와 올바르게 연결해야 합니다.
6. Docker와 CI/CD
6.1 Docker에서 Turbo를 쓰는 이유와 한계
컨테이너는 재현 가능한 빌드 환경을 주지만, 레이어 캐시가 얕으면 npm ci와 빌드가 매번 처음부터 실행됩니다. Turbo 원격 캐시를 컨테이너 빌드에 연결하면 같은 해시의 작업을 다시 하지 않아도 됩니다. 다만 네트워크 의존(캐시 서버 접근)과 빌드 인자가 해시에 미치는 영향을 함께 설계해야 합니다.
6.2 멀티 스테이지·의존성 레이어 분리 (개념 예시)
# 개념 예시: 실제 베이스 이미지·패키지 매니저는 프로젝트에 맞게 조정
FROM node:20-bookworm AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
COPY pnpm-workspace.yaml ./
COPY apps ./apps
COPY packages ./packages
RUN corepack enable && pnpm install --frozen-lockfile
FROM deps AS builder
ARG TURBO_TOKEN
ARG TURBO_TEAM
ENV TURBO_TOKEN=$TURBO_TOKEN
ENV TURBO_TEAM=$TURBO_TEAM
RUN pnpm turbo run build --filter=web-app...
위 패턴의 요지는 의존성 설치 레이어와 소스 복사 레이어를 분리해, package.json이 바뀌지 않는 한 pnpm install 캐시를 최대한 살리는 것입니다. 이어서 Turbo 원격 캐시 환경 변수를 빌드 스테이지에 주입해 CI와 동일한 캐시를 재사용합니다.
6.3 CI 파이프라인 설계
GitHub Actions 기준으로 흔한 패턴은 다음과 같습니다.
- 체크아웃·패키지 매니저 캐시 (
pnpm store등) - install
turbo run lint test build를 한 번에 또는 단계 분리- 원격 캐시용 시크릿 주입 (fork PR에는 읽기 전용 또는 비활성화)
PR 파이프라인에서 악의적인 워크플로가 캐시에 쓰기하는 것을 막으려면 권한 분리가 필요합니다. 조직 정책에 따라 pull_request_target 대신 pull_request에서만 돌리거나, 캐시 쓰기는 main 병합 후 파이프라인으로 제한하기도 합니다.
6.4 병렬 매트릭스와 중복 빌드
매트릭스로 OS·Node 버전을 바꾸면 캐시 키가 달라져 원격 캐시 이득이 줄어듭니다. 반드시 필요한 조합만 매트릭스에 넣고, 나머지는 단일 환경에서 통과시키는 편이 비용 대비 효과가 좋습니다.
7. Changesets 통합
7.1 역할 분담: Turbo vs Changesets
- Turborepo: 무엇을 어떤 순서로 실행할지, 캐시를 어떻게 쓸지.
- Changesets: 버전 범프, CHANGELOG, npm 배포 순서를 사람이 리뷰 가능한 흐름으로 만든다.
둘은 경쟁 관계가 아니라 서로 다른 층입니다.
7.2 워크플로 개요
- 기능 개발자가 changeset 파일을 추가한다(어떤 패키지가 major/minor/patch인지).
- 릴리스 담당이
changeset version으로 버전과 changelog를 갱신한다. pnpm turbo run build --filter=...로 배포 전 검증을 한다.changeset publish(또는 조직 표준 스크립트)로 npm에 올린다.
Turbo 쪽에서는 build·test·lint가 패키지 그래프에 맞게 이미 정의되어 있어야 하고, Changesets는 버전 의존성 그래프를 따라 패키지를 올바른 순서로 배포하려 합니다. 여기서 워크스페이스 의존성이 누락되어 있으면, 빌드는 되는데 배포 순서만 틀어지는 클래스의 문제가 생깁니다.
7.3 package.json 스크립트 정리 예시
{
"scripts": {
"ci": "turbo run lint test build",
"release": "changeset publish"
}
}
실제 조직에서는 release 앞뒤로 태그 생성, GitHub Release, Slack 알림이 붙습니다. 중요한 점은 릴리스 파이프라인도 코드로 두고, Turbo 태스크와 중복 실행이 없게 만드는 것입니다.
8. 실전: 대규모 모노레포 관리
8.1 소유권과 경계
수백 명이 한 저장소에 들어오면 “누가 이 패키지를 깨뜨렸는가”가 정치가 됩니다. CODEOWNERS, 패키지별 담당 팀 라벨, 아키텍처 결정 기록(ADR)이 Turborepo와 직접 연결되지는 않지만, 태스크 실패의 책임 범위를 명확히 하려면 필수에 가깝습니다.
8.2 “공용 유틸”의 팽창
packages/shared가 모든 것을 삼키면 의존성 지옥이 됩니다. 실무에서는 도메인별 패키지(packages/auth, packages/billing)로 쪼개고, 순환 참조를 ESLint·dependency-cruiser로 차단합니다. Turborepo 파이프라인은 이 구조를 그대로 반영해야 합니다.
8.3 캐시·CI 비용 모니터링
원격 캐시는 빌드 시간을 줄이지만, 저장·전송 비용과 캐시 적중률을 보면서 튜닝해야 합니다. 주간 리포트에 “평균 빌드 시간, 캐시 미스율, 가장 무거운 태스크 Top N”을 올리면, 병목 패키지를 데이터로 고칠 수 있습니다.
8.4 점진적 마이그레이션
레거시 패키지를 한 번에 옮기기보다, 신규 패키지부터 Turbo 파이프라인·워크스페이스 규칙을 적용하고, 레거시는 필터로 단계적으로 turbo run에 편입합니다. “전부 또는 전무”로 가면 중도 이탈이 많습니다.
9. 트러블슈팅 체크리스트
| 증상 | 우선 확인 |
|---|---|
| CI만 느리다 | 원격 캐시 토큰·팀 ID, 네트워크, turbo 버전 불일치 |
| 로컬은 캐시 히트인데 CI는 미스 | 환경 변수·OS·경로 대소문자·시간 의존 빌드 |
| 특정 패키지만 매번 전체 재빌드 | outputs 누락, 잘못된 dependsOn, 숨은 의존성 |
| 테스트만 이상하게 통과 | ^build 없이 오래된 dist를 참조 |
10. 맺음말
Turborepo의 심화 주제는 결국 빌드 그래프를 팀의 사실(fact)과 일치시키는 일입니다. 원격 캐시는 그 사실을 조직 전체로 전파하는 수단이고, 파이프라인 최적화는 사실을 흐리지 않게 정제하는 과정이며, Docker·CI·Changesets는 그 사실을 배포와 릴리스까지 일관되게 이어 주는 장치입니다. 이 글의 패턴을 자신의 저장소 규모에 맞게 자르고 붙이면, 모노레포가 “커질수록 느려지는 곳”이 아니라 “커질수록 이점이 누적되는 곳”으로 바뀔 수 있습니다.
참고 및 버전 유의
Turborepo는 활발히 발전하는 도구이므로, turbo.json 키 이름(pipeline vs tasks)과 CLI 옵션은 프로젝트에 고정된 메이저 버전의 공식 문서를 기준으로 맞추시기 바랍니다. 이 글의 설정 예시는 개념 설명용이며, 복사 후 그대로 쓰기 전에 팀의 패키지 구조에 맞게 조정해야 합니다.