Turborepo
모노레포에서 여러 패키지를 관리하다 보면 빌드 순서와 중복 작업이 문제가 된다. 프론트엔드, 백엔드, 공통 라이브러리가 한 저장소에 있을 때, 공통 라이브러리를 먼저 빌드하고 나서 프론트와 백엔드를 빌드해야 하는 의존 관계가 생긴다. 패키지가 5개, 10개로 늘어나면 이 순서를 수동으로 관리하는 건 사실상 불가능하다. 쉘 스크립트로 cd packages/common && npm run build && cd ../frontend && npm run build && ... 이런 식으로 짤 수도 있지만, 의존 관계가 복잡해지면 금방 한계에 부딪힌다.
더 큰 문제는 불필요한 반복 빌드다. 공통 라이브러리 코드를 한 줄도 안 건드렸는데, 프론트엔드만 수정한 상황에서도 전체를 다시 빌드하게 된다. CI에서 매번 5분씩 걸리는 빌드가 실은 30초면 끝날 수 있는 작업이었다면? 이런 낭비가 모노레포의 규모가 커질수록 심해진다.
Turborepo는 이 두 가지 문제를 해결한다. 태스크 간 의존 관계를 선언적으로 정의하고, 변경되지 않은 패키지의 빌드 결과를 캐싱해서 건너뛴다.
핵심 개념: 태스크 파이프라인
Turborepo의 핵심은 turbo.json에 정의하는 태스크 파이프라인이다. 각 태스크가 어떤 순서로 실행되어야 하는지, 어떤 입력이 변경되면 다시 실행해야 하는지, 어떤 출력을 캐싱할지를 선언한다.
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
이 설정만으로 Turborepo는 모노레포 전체의 빌드 그래프를 이해한다. turbo run build를 실행하면 각 패키지의 build 스크립트를 의존 순서에 맞게 실행하고, 독립적인 패키지끼리는 병렬로 처리한다.
dependsOn: 실행 순서 제어
dependsOn은 태스크 간 의존 관계를 정의한다. 여기서 ^ 접두사가 핵심적인 의미를 가진다.
^ 접두사 — 의존 패키지 우선
{
"build": {
"dependsOn": ["^build"]
}
}
^build는 "이 패키지가 의존하는 다른 패키지들의 build가 먼저 완료되어야 한다"는 뜻이다. 패키지 A가 공통 라이브러리 B에 의존한다면, B의 build가 끝나야 A의 build가 시작된다.
이 의존 관계는 package.json의 dependencies/devDependencies에서 자동으로 추론된다. Turborepo가 워크스페이스의 패키지 그래프를 분석해서 순서를 결정하기 때문에, 개발자가 빌드 순서를 직접 관리할 필요가 없다.
^ 없이 — 같은 패키지 내 선행 태스크
{
"test": {
"dependsOn": ["build"]
}
}
^ 없이 "build"만 쓰면, 같은 패키지 안에서 build가 먼저 실행되어야 test를 실행한다는 뜻이다. 다른 패키지의 build와는 무관하다.
혼합 사용
{
"test": {
"dependsOn": ["^build", "lint"]
}
}
이렇게 하면 "의존 패키지들의 build가 끝나고, 같은 패키지의 lint도 끝나야 test를 실행한다"는 의미가 된다. 두 가지 규칙을 조합해서 복잡한 파이프라인도 표현할 수 있다.
캐싱: 같은 일을 두 번 하지 않는다
Turborepo의 가장 강력한 기능은 콘텐츠 해시 기반 캐싱이다. 태스크를 실행할 때 입력 파일들의 해시를 계산하고, 이전 실행과 해시가 동일하면 캐시된 결과를 그대로 복원한다.
캐시 히트의 동작
- 태스크 실행 전, 입력 파일들의 해시를 계산한다
- 동일한 해시의 캐시가 있는지 확인한다
- 캐시가 있으면
outputs에 지정된 파일들을 캐시에서 복원한다 - 터미널 출력(stdout/stderr)도 재생한다
캐시가 히트하면 터미널에 이런 메시지가 나온다:
@chiki/frontend:build: cache hit, replaying logs
실제 빌드를 하지 않고 이전 결과를 그대로 쓰기 때문에, 수 분 걸리던 빌드가 수백 밀리초로 줄어든다.
inputs: 캐시 키 결정
{
"build": {
"inputs": ["$TURBO_DEFAULT$", ".env*"]
}
}
inputs는 어떤 파일이 변경되었을 때 캐시를 무효화할지 결정한다. $TURBO_DEFAULT$는 Turborepo의 기본 입력 집합으로, 패키지 내 소스 파일들(gitignore에 포함되지 않은 파일)을 의미한다. 여기에 .env*를 추가하면 환경 변수 파일이 바뀌었을 때도 다시 빌드한다.
inputs를 명시하지 않으면 패키지 내 모든 파일이 입력으로 간주된다. README 하나 수정해도 빌드가 다시 돌아갈 수 있으니, 적절히 지정하는 것이 좋다.
outputs: 캐시 저장 대상
{
"build": {
"outputs": ["dist/**"]
}
}
outputs는 캐시에 저장할 파일 패턴이다. 빌드 결과물이 dist/ 디렉토리에 생성된다면 이렇게 설정한다. 캐시 히트 시 이 파일들이 복원된다. outputs를 지정하지 않으면 터미널 출력만 캐싱되고 파일 복원은 하지 않는다.
캐시 비활성화
{
"dev": {
"cache": false,
"persistent": true
}
}
dev 서버처럼 지속적으로 실행되는 태스크는 캐싱이 의미 없다. cache: false로 캐싱을 끄고, persistent: true로 이 태스크가 종료되지 않는 장기 실행 프로세스임을 알려준다. persistent를 설정하면 Turborepo가 이 태스크의 완료를 기다리지 않고 다른 태스크를 계속 실행한다.
환경 변수와 캐시
환경 변수가 빌드 결과에 영향을 미치는 경우(API URL, 모드 설정 등), 이를 캐시 키에 포함시켜야 한다. 그렇지 않으면 환경 변수가 바뀌었는데 캐시된 이전 빌드를 쓰는 문제가 생긴다.
{
"build": {
"env": ["API_URL", "NODE_ENV"],
"passThroughEnv": ["CI"]
}
}
env: 이 환경 변수들의 값을 캐시 해시에 포함한다. 값이 바뀌면 캐시가 무효화된다.passThroughEnv: 캐시 해시에는 포함하지 않지만 태스크 실행 시 전달한다.CI같은 변수는 빌드 결과에 영향을 주지 않지만 로깅 등에 사용될 수 있다.
globalEnv를 turbo.json 최상위에 설정하면 모든 태스크에 공통으로 적용된다:
{
"globalEnv": ["DEPLOY_ENV"],
"tasks": { ... }
}
실행 방법
기본 실행
turbo run build # 모든 패키지의 build 실행
turbo run build lint # build와 lint를 동시에 실행
turbo run build --filter=@chiki/frontend # 특정 패키지만
--filter: 범위 제한
--filter는 특정 패키지만 대상으로 태스크를 실행한다. 패키지 이름, 디렉토리 경로, git 변경 범위 등 다양한 방식으로 필터링할 수 있다.
# 패키지 이름으로
turbo run build --filter=@chiki/frontend
# 디렉토리 경로로
turbo run build --filter=./apps/frontend
# 의존성 포함 (... 접미사)
turbo run build --filter=@chiki/frontend...
# git 변경 기준
turbo run build --filter=[HEAD^1]
... 접미사는 해당 패키지와 그 패키지가 의존하는 모든 패키지를 포함한다. 프론트엔드를 빌드하려면 공통 라이브러리도 빌드해야 하니까, --filter=@chiki/frontend...로 한 번에 처리할 수 있다.
병렬 실행
Turborepo는 의존 관계가 없는 태스크를 자동으로 병렬 실행한다. --concurrency 옵션으로 동시 실행 수를 제어할 수 있다:
turbo run build --concurrency=4 # 최대 4개 동시
turbo run build --concurrency=100% # CPU 코어 수만큼
기본값은 CPU 코어 수의 10개와 코어 수 중 작은 값이다.
태스크 그래프 시각화
복잡한 모노레포에서 태스크 실행 순서를 확인하고 싶을 때 --graph 옵션을 사용한다:
turbo run build --graph # 기본 (SVG 파일 생성)
turbo run build --graph=graph.png # PNG로 출력
turbo run build --dry=json # JSON으로 실행 계획 출력
--dry 옵션은 실제 실행 없이 어떤 태스크가 어떤 순서로 실행될지만 보여준다. CI 파이프라인을 디버깅할 때 유용하다.
Remote Cache: 팀 단위 캐시 공유
로컬 캐시만으로도 개인 개발에서는 충분하지만, CI 환경이나 팀원 간에 캐시를 공유하면 효과가 극대화된다. A 개발자가 빌드한 결과를 B 개발자가 그대로 쓸 수 있다.
# Vercel Remote Cache 연동 (Turborepo 공식)
npx turbo login
npx turbo link
Vercel 계정과 연동하면 자동으로 Remote Cache가 활성화된다. 자체 호스팅 캐시 서버를 쓸 수도 있다:
// turbo.json
{
"remoteCache": {
"enabled": true
}
}
Remote Cache의 동작은 간단하다. 태스크 해시를 키로 원격 저장소에 결과를 올리고, 같은 해시의 캐시가 있으면 다운로드해서 복원한다. CI에서 특히 효과적인데, 매 PR마다 전체 빌드를 돌리지 않아도 되기 때문이다.
워크스페이스 구성과의 관계
Turborepo 자체는 패키지 매니저가 아니다. pnpm, npm, yarn 같은 패키지 매니저의 워크스페이스 기능 위에서 동작한다. 패키지 간 의존 관계를 package.json에서 읽어오고, 빌드 오케스트레이션만 담당한다.
chiki/
├── turbo.json # Turborepo 설정
├── pnpm-workspace.yaml # 워크스페이스 정의
├── apps/
│ ├── frontend/ # React 프론트엔드
│ ├── admin/ # 어드민 대시보드
│ └── backend/ # NestJS 백엔드
└── packages/
└── shared/ # 공통 타입/유틸리티
이 구조에서 turbo run build를 실행하면:
pnpm-workspace.yaml에서 워크스페이스 패키지 목록을 읽는다- 각 패키지의
package.json에서 의존 관계를 파악한다 turbo.json의 파이프라인 정의에 따라 실행 순서를 결정한다- 캐시를 확인하고, 필요한 태스크만 실행한다
Nx와의 비교
모노레포 빌드 도구로 Nx도 많이 쓰인다. 둘의 접근 방식은 꽤 다르다.
| 비교 항목 | Turborepo | Nx |
|---|---|---|
| 설정 복잡도 | 낮음 (turbo.json 하나) | 높음 (nx.json + project.json 등) |
| 기능 범위 | 빌드 오케스트레이션 특화 | 코드 생성, 마이그레이션 등 올인원 |
| 캐싱 | 기본 내장 | 기본 내장 |
| 플러그인 | 없음 (단순함 유지) | 풍부한 플러그인 생태계 |
| 학습 곡선 | 낮음 | 중간~높음 |
Turborepo는 "빌드를 빠르게 돌리는 것"에 집중한다. 설정이 단순하고 기존 프로젝트에 도입하기 쉽다. 반면 Nx는 모노레포 관리의 전반을 커버하는 풀 프레임워크에 가깝다. 이미 잘 동작하는 프로젝트에 빌드 캐싱만 추가하고 싶다면 Turborepo가, 처음부터 모노레포를 체계적으로 구성하고 싶다면 Nx가 적합하다.
실전 팁
package.json에 turbo 스크립트 등록
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint"
}
}
루트 package.json에 등록해두면 pnpm build로 전체 빌드를 실행할 수 있다.
.gitignore에 캐시 디렉토리 추가
# Turborepo
.turbo
.turbo/ 디렉토리에 로컬 캐시가 저장된다. 커밋할 필요 없으니 gitignore에 추가한다.
CI에서의 캐시 전략
CI 환경에서는 .turbo/ 디렉토리를 CI 캐시로 보존하면 Remote Cache 없이도 캐싱 효과를 얻을 수 있다:
# GitHub Actions 예시
- uses: actions/cache@v3
with:
path: .turbo
key: turbo-${{ github.sha }}
restore-keys: turbo-
캐시 강제 무효화
캐시가 오래되었거나 문제가 의심될 때:
turbo run build --force # 캐시 무시하고 전체 재빌드
로컬 캐시를 완전히 삭제하려면 .turbo/ 디렉토리를 삭제하면 된다.
정리
dependsOn의^접두사로 패키지 간 빌드 순서를 선언하고, 독립적인 태스크는 자동 병렬 실행된다- 콘텐츠 해시 기반 캐싱으로 변경되지 않은 패키지의 빌드를 건너뛰며,
inputs/outputs/env설정이 캐시 정확도를 결정한다 - 패키지 매니저 워크스페이스 위에서 동작하므로 기존 모노레포에
turbo.json하나로 도입할 수 있고,--filter와--graph로 범위와 디버깅을 제어한다