모노레포 CI 14분을 3분으로 단축한 turbo.json 설정 — inputs·env·GitHub Actions 캐시 히트율 실전 튜닝
캐시 히트율 4%짜리를 87%로 끌어올린 설정 변경은 딱 세 줄이었습니다. turbo.json에서 env와 passThroughEnv를 제대로 분리한 것뿐이었는데, CI가 14분에서 3분으로 줄었습니다.
20개 패키지짜리 모노레포를 운영하다 보면 어느 순간 CI가 너무 느려져서 PR 하나 올리는 게 두려워지는 시점이 옵니다. 팀원들이 "빌드 기다리는 동안 뭐 하지?"라는 말을 반복하기 시작했을 때, 제대로 파고든 게 Turborepo의 원격 캐시와 태스크 그래프 최적화였습니다. 이미 Turborepo를 쓰고 있다면 설치나 기본 설정보다 지금 당장 캐시 히트율을 올리는 inputs/env 튜닝이 훨씬 중요합니다.
이 글에서는 Turborepo가 어떻게 빌드를 가속하는지 원리부터 짚고, GitHub Actions에서 실제로 동작하는 설정까지 단계별로 살펴봅니다. turbo.json의 outputs, inputs, env를 올바르게 설정하는 것만으로도 CI 시간을 70~80% 단축할 수 있습니다. Turborepo 2.x 기준의 최신 브레이킹 체인지와 버전별 주요 변화도 함께 정리했습니다.
이미 Turborepo 기본 설정을 알고 계신다면 실전 적용으로 바로 이동하셔도 됩니다.
핵심 개념
원격 캐시: "이미 만든 결과물을 왜 또 만들어요?"
Turborepo의 원격 캐시를 한 문장으로 정리하면 이렇습니다. 입력값이 같다면 결과물도 같을 테니, 굳이 다시 빌드하지 말고 이전에 저장해둔 결과를 그대로 가져오자는 거죠.
입력값 판단은 SHA-256 해시를 기반으로 합니다. 소스 파일, package.json 의존성, 환경 변수, turbo.json 설정을 조합해 해시를 만들고, 해시가 동일하면 "캐시 히트"로 판정해 저장된 결과물을 복원합니다. 로컬 캐시는 수 밀리초 안에 복원되고, 원격 캐시는 네트워크 다운로드가 필요해 수백 밀리초~수 초 단위지만, 실제 빌드 시간 대비 수십 배 빠릅니다.
캐시 히트(Cache Hit): 입력 해시가 이전 실행과 동일해 빌드를 재실행하지 않고 저장된 결과물을 바로 사용하는 상태. 반대는 캐시 미스(Cache Miss)로, 이 경우 태스크를 실제로 실행합니다.
원격 캐시의 진짜 강점은 팀 전체가 같은 캐시를 공유한다는 점입니다. 동료가 main 브랜치에서 빌드를 이미 돌렸다면, 내가 같은 커밋으로 빌드할 때 그 결과물을 로컬로 당겨올 수 있습니다. CI에서 빌드된 결과를 내 맥북에서 쓰고, 내 맥북에서 빌드된 결과를 CI가 쓰는 것도 가능합니다.
태스크 그래프: 병렬로 돌릴 수 있는 건 전부 병렬로
turbo.json의 dependsOn 설정을 바탕으로 Turborepo는 패키지와 태스크 간 의존 관계를 방향 비순환 그래프(DAG, Directed Acyclic Graph) 로 모델링합니다. 의존 관계가 없는 태스크는 자동으로 병렬 실행하고, 의존이 있는 것만 순서대로 직렬 실행합니다.
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": [],
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"inputs": ["src/**/*.ts", "test/**/*.ts"]
}
}
}Turborepo 2.0 브레이킹 체인지: 기존
pipeline필드가tasks로 이름이 바뀌었습니다. 1.x에서 마이그레이션할 때 가장 많이 부딪히는 변경점이라 꼭 확인해두시면 좋습니다.
여기서 ^build의 ^ 접두사가 핵심입니다. "이 패키지가 의존하는 모든 패키지의 build가 먼저 완료되어야 한다"는 뜻입니다. lint의 dependsOn이 빈 배열이면, 다른 태스크와 상관없이 즉시 병렬 실행됩니다.
토폴로지 의존성 (
^):^task는 현재 패키지의 의존성 트리 전체에서 해당 태스크가 완료된 후 실행을 의미합니다. 반면 캐럿 없는task는 동일 패키지 내의 태스크 순서를 제어할 때 씁니다.
outputs를 올바르게 정의하는 것도 빠뜨릴 수 없습니다. Turborepo는 outputs에 정의된 파일을 캐시 아티팩트로 저장하기 때문에, 이게 없으면 캐시 복원 자체가 불가능합니다. 저도 초기 설정 때 이걸 빠뜨려서 "왜 캐시가 안 되지?" 하고 한참 헤맸던 기억이 있어요.
--affected가 내부적으로 하는 일
--affected 플래그는 내부적으로 git diff를 기반으로 작동합니다. PR의 베이스 브랜치와 헤드 브랜치를 비교해 실제로 변경된 파일을 파악하고, 그 파일을 포함하는 패키지와 그 의존 패키지만 실행 대상으로 삼습니다. 이 때문에 전체 git 히스토리가 없으면 비교 자체가 불가능해집니다. GitHub Actions에서 fetch-depth: 0이 필수인 이유가 바로 이것입니다. 이 맥락을 먼저 알아두면 나중에 실수 항목이 훨씬 자연스럽게 이해됩니다.
Turborepo 2.x의 달라진 점
2025~2026년 2.x 시리즈에서 체감 성능이 크게 올라왔습니다.
| 버전 | 주요 변화 |
|---|---|
| 2.5 | 사이드카 태스크, turbo.jsonc 주석 지원, $TURBO_ROOT$ 변수 |
| 2.7 | 태스크 그래프 시각화 Devtools, 패키지별 turbo.json 컴포저블 설정 |
| 2.9 | time-to-first-task 96% 개선(★), turbo query 안정화, OpenTelemetry 실험적 지원 |
★ 두 가지 지표를 구분해두면 좋습니다. 태스크 그래프 계산 속도가 81~91% 향상된 것과, 첫 번째 태스크가 실행 시작되는 지연이 96% 줄어든 것은 별개의 측정치입니다. 전자는 의존 관계 분석 시간, 후자는 turbo run을 입력하고 실제로 무언가가 돌기까지의 체감 속도입니다. 200개 패키지가 넘는 레포에서 turbo run을 치면 즉각 반응한다는 게 처음엔 믿기지 않을 정도였습니다.
실전 적용
예시 1: GitHub Actions 원격 캐시 + affected 전략
가장 빠르게 효과를 볼 수 있는 조합입니다. Vercel 원격 캐시와 --affected 플래그를 함께 쓰면 PR에서 변경된 패키지만 빌드하면서 이전 캐시도 활용할 수 있습니다.
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # git diff 기반 --affected 계산에 전체 히스토리가 필요합니다
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Build & Test (affected only)
run: pnpm turbo run build test lint --affected
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}| 설정 | 역할 |
|---|---|
fetch-depth: 0 |
PR 베이스·헤드 브랜치 간 git diff 계산을 위해 전체 git 히스토리 필요 |
--affected |
변경된 패키지와 그 의존 패키지만 실행, 나머지는 캐시 히트 처리 |
TURBO_TOKEN |
Vercel 원격 캐시 인증 토큰 — GitHub Secrets에 저장 |
TURBO_TEAM |
원격 캐시 팀 식별자 — GitHub Variables에 저장 |
20개 패키지 워크스페이스 기준으로 이 설정만 적용해도 CI가 14분에서 35분으로 줄어드는 경험을 했습니다. PR 하나에서 실제로 바뀌는 패키지는 보통 13개 정도거든요.
대안: S3 자체 호스팅 원격 캐시
Vercel 원격 캐시가 유료라 비용이 부담스럽다면, ducktors/turborepo-remote-cache 오픈소스로 S3 기반 자체 서버를 구축하는 방법도 있습니다. 솔직히 인프라 관리 부담이 생기긴 하지만, 팀 규모가 크다면 비용 절감 효과가 더 클 수 있습니다.
아래처럼 서버 환경 변수를 구성하고, Docker Compose 등으로 서버를 실행하시면 됩니다. 서버 구축의 세부 절차는 ducktors/turborepo-remote-cache README를 참고하시면 좋습니다.
# 원격 캐시 서버 환경 변수
STORAGE_PROVIDER=s3
STORAGE_PATH=my-turbo-cache-bucket
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=ap-northeast-2
TURBO_TOKEN=my-secret-token서버를 띄웠다면 프로젝트에 .turbo/config.json으로 커스텀 서버 주소를 지정합니다. 이 파일은 자체 호스팅 서버를 쓸 때만 필요하며, Vercel 원격 캐시를 쓴다면 환경 변수만으로 충분합니다.
// .turbo/config.json (자체 호스팅 전용)
{
"teamId": "team_my-org",
"apiUrl": "https://my-cache-server.example.com"
}캐시 히트율 더 끌어올리기: inputs/env 세밀 조정
원격 캐시를 붙였는데도 캐시 미스가 너무 많다면, inputs와 env 설정을 들여다볼 필요가 있습니다. 저도 처음엔 왜 캐시 히트가 이렇게 낮지 싶었는데, 알고 보니 CI마다 값이 달라지는 환경 변수들이 해시에 전부 포함되고 있었던 거였어요. 이 부분이 실무에서 가장 디테일이 필요한 영역입니다.
{
"tasks": {
"test": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "!README.md", "!**/*.md"],
"outputs": ["coverage/**"]
},
"build": {
"dependsOn": ["^build"],
"env": ["API_URL", "NODE_ENV"],
"passThroughEnv": ["SENTRY_AUTH_TOKEN"],
"outputs": [".next/**", "!.next/cache/**"]
}
}
}| 설정 | 역할 |
|---|---|
$TURBO_DEFAULT$ |
기본 inputs(소스 파일 전체)를 유지하면서 추가 제외 규칙 적용 |
!README.md |
README 변경이 캐시 해시에 영향 주지 않도록 제외 |
env |
빌드 결과에 실제로 영향을 주는 환경 변수만 해시에 포함 |
passThroughEnv |
해시에는 포함하지 않지만 태스크 실행 시 전달할 환경 변수 |
passThroughEnv는 Turborepo 2.0 이후 생긴 개념인데, Sentry 인증 토큰처럼 빌드 결과물 자체에는 영향 없지만 실행 중에는 필요한 환경 변수에 씁니다. 해시에 포함시키지 않으니 CI마다 토큰 값이 달라도 캐시 히트가 유지됩니다. 이 하나 설정으로 캐시 히트율이 극적으로 올라가는 경우가 많습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 설정 단순성 | turbo.json 하나로 전체 파이프라인 구성, 기존 모노레포에 10분 안에 도입 가능 |
| 원시 성능 | Rust 기반 엔진, 태스크 그래프 계산 81~91% 단축, time-to-first-task 최대 96% 개선 |
| 자동 병렬화 | 의존 관계 없는 태스크를 자동 병렬 실행, 별도 설정 불필요 |
| 팀 캐시 공유 | CI ↔ 로컬 간 캐시 공유로 "이미 빌드된" 결과를 동료와 재사용 |
| 자체 호스팅 | Vercel 의존 없이 S3, GCS, Azure Blob 등 다양한 스토리지에 구축 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 원격 캐시 비용 | Vercel 원격 캐시 무료 플랜 제한, 팀 규모 커질수록 유료 | S3 자체 호스팅으로 전환 고려 |
| 환경 변수 해시 포함(2.0+) | 기본적으로 환경 변수가 해시에 포함되어 캐시 미스 증가 가능 | env / passThroughEnv 세밀 분류 |
--affected 정확도 |
일부 엣지 케이스에서 변경 안 된 패키지가 affected로 잘못 인식 | turbo query affected로 사전 검증 |
| CI 분산 실행 한계 | Nx Agents처럼 여러 CI 머신에 자동 분산하는 기능 없음 | 초대형 모노레포는 수동 분산 설정 필요 |
| 로그 아티팩트 주의 | 콘솔 출력이 캐시 아티팩트로 저장됨 | 민감 정보를 로그에 출력하지 않도록 주의 |
실무에서 가장 흔한 실수
-
outputs를 정의하지 않는 것: 빌드 태스크에outputs가 없으면 Turborepo가 무엇을 캐시해야 할지 모릅니다. 캐싱이 아예 동작하지 않는 가장 흔한 원인입니다..next/**,dist/**,coverage/**같은 빌드 결과물 경로를 반드시 지정해두시면 좋습니다. -
fetch-depth: 0없이--affected사용:--affected는 내부적으로git diff를 씁니다. git 히스토리가 없으면 PR 베이스와 헤드를 비교할 수 없어 모든 패키지를 affected로 처리합니다.actions/checkout의fetch-depth: 0은--affected와 세트라고 기억해두시면 좋습니다. -
환경 변수를
env와passThroughEnv로 구분하지 않는 것: 저도 이걸 한동안 몰랐습니다. Turborepo 2.0 이후 환경 변수가 기본적으로 해시에 포함됩니다. CI 환경마다 다른 값을 가지는 환경 변수(예: 인증 토큰, 배포 URL)를env에 넣으면 캐시 미스가 폭발적으로 늘어납니다. 빌드 결과물에 실제로 영향을 주는 것만env에, 나머지는passThroughEnv에 분리해두는 것을 권장합니다.
마치며
turbo.json의 outputs, inputs, env를 올바르게 설정하는 것만으로도 CI 시간을 절반 이상 줄일 수 있습니다. 원격 캐시와 --affected 전략까지 함께 적용하면, 14분 CI가 3분으로 바뀌는 경험을 직접 해보실 수 있을 겁니다.
지금 바로 시작해볼 수 있는 3단계입니다.
turbo.json점검부터 시작해보시면 좋습니다. 기존build,test,lint태스크에outputs가 빠져 있는지 확인하고,.next/**,dist/**,coverage/**등 빌드 결과물 경로를 추가하시면 됩니다.- Vercel 원격 캐시를 연결해보시면 좋습니다.
TURBO_TOKEN과TURBO_TEAM을 GitHub Secrets/Variables에 추가하고 CI 스크립트에 넣어주시면 됩니다 (비용이 걱정된다면ducktors/turborepo-remote-cache+ S3 자체 호스팅도 좋은 선택입니다). - GitHub Actions에
--affected플래그와fetch-depth: 0을 추가해보시면 좋습니다. PR마다 전체 빌드 대신 변경된 패키지만 실행하도록 바꾸면 팀 규모가 클수록 체감 효과가 커집니다.
다음 편
Turborepo 환경에서 패키지 간 공유 설정(ESLint, TypeScript, Tailwind)을 packages/config-* 패턴으로 관리하고, 버전 없이 워크스페이스 참조로 연결하는 실전 구성 가이드를 다룰 예정입니다.
참고 자료
- Remote Caching | Turborepo 공식 문서
- Package and Task Graphs | Turborepo 공식 문서
- Caching | Turborepo 공식 문서
- Configuring Tasks | Turborepo 공식 문서
- Using Environment Variables | Turborepo 공식 문서
- GitHub Actions | Turborepo 공식 문서
- Turborepo 2.9 Release Blog
- Turborepo 2.7 Release Blog
- Turborepo 2.5 Release Blog
- Making Turborepo 96% Faster | Vercel Blog
- Remote Caching | Vercel Docs
- ducktors/turborepo-remote-cache | GitHub
- Setting Up Turborepo Remote Cache with S3 and GitHub Actions