Turborepo는 JavaScript·TypeScript 모노레포의 태스크(빌드·린트·테스트)를 빠르게 돌리는 빌드 시스템이다. 기존 package.json 스크립트는 그대로 두고, 스크립트 사이의 의존 관계만 알려 주면 병렬 실행과 캐싱을 맡는다. 의도적으로 "빠른 실행과 캐시" 한 가지에 집중한다.
태스크 그래프와 병렬 실행
turbo.json에 태스크 사이의 관계를 선언하면, Turborepo는 그 그래프가 허락하는 만큼 병렬로 돌린다.
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"test": { "dependsOn": ["build"] },
"deploy": { "dependsOn": ["utils#build"], "cache": false },
"dev": { "persistent": true, "cache": false },
"lint": {}
}
}^build: 의존하는 패키지들의 build가 먼저다(위상 순서). 라이브러리를 먼저 빌드하고 그것을 쓰는 앱을 빌드한다build(접두사 없음): 같은 패키지 안의 순서다. test 전에 그 패키지의 build를 끝낸다utils#build: 특정 패키지의 태스크를 기다린다persistent: true: dev 서버처럼 끝나지 않는 태스크다. 다른 태스크가 이것에 의존할 수 없다. 함께 띄울 태스크는with에 적는다- 동시 실행 수는
--concurrency로 정한다. 기본값은 10이고100%면 논리 코어 수만큼 쓴다. 그래프를 무시하는--parallel은 deprecated다(2026 기준) - 타입 검사처럼 "의존 패키지가 바뀌면 다시 돌아야 하지만 순서는 상관없는" 태스크는 transit 노드로 푼다. 스크립트가 없는 빈 태스크
transit: { "dependsOn": ["^transit"] }를 두고check-types: { "dependsOn": ["transit"] }로 걸면, 의존 관계는 해시에 반영되면서 실행은 병렬로 된다.^check-types로 걸면 캐시는 정확하지만 직렬로 돈다
캐시: 무엇으로 해시를 만들고 무엇을 저장하나
해시는 두 층이고 어느 쪽이 바뀌어도 미스(miss)다.
| 층 | 들어가는 것 |
|---|---|
| 전역 해시(모든 태스크) | 루트 turbo.json의 태스크 정의, 루트 lockfile 변경, globalDependencies에 적은 파일(예: .env), globalEnv 변수 값, 동작을 바꾸는 플래그, -- 뒤의 패스스루 인자 |
| 패키지 해시(그 패키지) | 패키지의 package.json과 turbo.json, 그 패키지 의존성에 해당하는 lockfile 부분, 소스 파일(기본은 git이 추적하는 전부, inputs로 좁힘), env 변수 값 |
- 저장하는 것은
outputs에 적은 파일과 터미널 로그다. 히트(hit)면 파일을 복원하고 로그를 재생한다. 모든 태스크가 히트면FULL TURBO로 끝난다 inputs를 적으면 기본 동작(.gitignore존중 포함)이 사라진다. 기본에 덧붙이려면["$TURBO_DEFAULT$", "!README.md"]처럼 쓴다- 환경변수는 기본이 strict 모드다.
env·globalEnv·passThroughEnv에 적지 않은 변수는 해시에도 안 들어가고 태스크에도 전달되지 않는다.passThroughEnv는 해시에는 넣지 않고 런타임에만 넘기는 변수다(예:NODE_OPTIONS). Next.js의NEXT_PUBLIC_*, Vite의VITE_*는 프레임워크 추론으로 자동 포함된다..env파일은 로드해 주지 않으니 해시에 넣으려면inputs에.env*를 적는다 - 로컬 캐시는
.turbo/cache에 있고,turbo login→turbo link로 원격 캐시(remote caching)를 켜면 팀과 CI가 결과물을 공유한다. CI가 main에서 이미 빌드한 패키지를 내 컴퓨터는 다시 빌드하지 않는다 - 미스 원인은
--dry=json(태스크별 해시·inputs·outputs)과--summarize(.turbo/runs에 실행 요약 저장)로 두 번 실행을 비교해 찾는다
바뀐 것만 돌리기
turbo run test --affected # main...HEAD 사이에 바뀐 패키지와 그 의존자
turbo run build --filter=...[origin/main] # 비슷한 뜻을 filter 문법으로
turbo run build --filter=@acme/ui... # ui와 ui가 의존하는 것
turbo run lint --filter=./apps/* --filter=!./apps/admin--affected의 기준 커밋은 TURBO_SCM_BASE로 바꾼다. CI에서 얕은 clone이면 base와 head 사이 이력이 없어 전부 바뀐 것으로 본다.
하지 않는 것
- 코드 생성기와 아키텍처 규칙은 거의 없다.
turbo boundaries가 패키지 디렉터리 밖 파일 import와package.json에 없는 의존성 import를 잡고 태그 기반 allow/deny 규칙을 받지만, 실험 단계다(2026 기준). 그 이상은 Nx의 모듈 경계 규칙이나 별도 린트 규칙을 쓴다 - 캐시는 태스크가 결정적일 때만 안전하다. 라이브 백엔드를 치는 e2e처럼 입력이 같아도 결과가 달라지는 태스크는
cache: false로 둔다. 원격 왕복보다 빠른 태스크나 결과물이 아주 큰 태스크는 캐시가 오히려 느릴 수 있다 - 잘 맞는 곳: 패키지 수가 수~수십 개인 JS·TS 저장소, pnpm 워크스페이스와 함께 쓸 때. 덜 맞는 곳: 복잡한 코드 생성, 여러 CI 머신에 작업 분산, JS 외 언어가 중심인 경우
흔히 권하는 순서는 워크스페이스로 시작 → 빌드가 느려지면 Turborepo → 더 많은 구조가 필요하면 Nx다(2026 기준). 입력 해시로 결과를 재사용하는 원리는 Dockerfile: 레이어 캐시와 멀티 스테이지의 레이어 캐시와 같고, 모노레포 자체는 모노레포, 번들러와의 관계는 번들러를 본다.
출처: Turborepo: Caching · Configuring tasks · Using environment variables · turbo run 레퍼런스 · Remote Caching · turbo boundaries