왜 모노레포에서 태스크 실행이 문제가 되는가
패키지가 20개, 30개로 늘어나면 npm run build 한 번이 수십 분으로 늘어난다. 대부분은 바뀌지 않은 패키지를 다시 빌드하는 낭비다. 문제의 핵심은 세 가지다. 첫째, 태스크 간 의존 순서(예: ui 빌드 후 web 빌드)를 사람이 관리한다. 둘째, 변경되지 않은 결과물도 매번 다시 만든다. 셋째, CI에서 로컬 캐시가 공유되지 않아 같은 작업을 반복한다. Turborepo와 Nx는 이 세 가지를 태스크 그래프, 콘텐츠 해시 캐시, 원격 캐시로 해결한다.
태스크 그래프: 순서를 선언으로 바꾸기
두 도구 모두 "무엇이 무엇에 의존하는가"를 선언하면 실행 순서와 병렬화를 자동으로 계산한다. Turborepo의 ^build는 "의존 패키지의 build를 먼저"라는 뜻이다.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {}
}
}
outputs를 정확히 지정하는 것이 중요하다. 여기 명시된 경로가 캐시에 저장·복원되는 산출물이다. 빠뜨리면 캐시 히트인데도 dist가 비어 있는 상황이 생긴다.
콘텐츠 해시 기반 캐시
캐시 키는 시간이 아니라 입력의 해시다. 소스 파일, 의존성 락파일, 환경변수, 태스크 설정을 해시해서 동일하면 실행을 건너뛰고 산출물을 복원한다. 환경변수가 빌드 결과에 영향을 준다면 반드시 env에 선언해야 한다. 선언하지 않으면 값이 달라져도 같은 캐시를 재사용해 잘못된 산출물이 나온다.
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": ["NODE_ENV", "NEXT_PUBLIC_API_URL"],
"outputs": ["dist/**"]
}
}
}
CI에서 변경분만 실행하기
PR에서는 바뀐 패키지와 그 하위 의존만 돌리면 된다. --filter로 base 브랜치 대비 변경분을 잡는다.
# 변경된 패키지와 그에 영향받는 패키지만 빌드/테스트
turbo run build test --filter="...[origin/main]"
# Nx의 동일 개념 (affected)
npx nx affected -t build test --base=origin/main --head=HEAD
이 한 줄이 CI 시간을 크게 줄인다. 단, fetch-depth: 0으로 전체 히스토리를 받아야 base 비교가 동작한다. shallow clone이면 변경 감지가 어긋난다.
원격 캐시로 팀·CI 전체 공유
로컬 캐시는 내 머신에만 있다. 원격 캐시를 붙이면 동료가 이미 빌드한 결과나 이전 CI 산출물을 그대로 내려받는다. 처음 통과한 사람 이후로는 사실상 복원만 일어난다.
# GitHub Actions
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: npm ci
- run: turbo run build test
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
사내 정책상 외부 캐시 서버를 못 쓴다면 S3 등 자체 스토리지를 캐시 백엔드로 두는 구성을 검토한다. 캐시에 비밀값이 섞여 들어가지 않도록 outputs 범위를 좁게 유지하는 게 안전하다.
Turborepo와 Nx, 무엇을 고를까
| 항목 | Turborepo | Nx |
|---|---|---|
| 설정 철학 | 기존 스크립트에 얇게 얹음 | 플러그인·제너레이터 중심 통합 |
| 학습 곡선 | 낮음 | 상대적으로 높음 |
| 코드 생성/스캐폴딩 | 없음 | 강력함 |
| 적합한 상황 | 이미 스크립트가 잡힌 JS/TS 레포 | 규모 큰 다언어·구조화된 워크스페이스 |
기존 package.json 스크립트를 그대로 두고 캐시·병렬화만 얹고 싶으면 Turborepo가 빠르다. 코드 생성, 모듈 경계 강제, 다양한 플러그인이 필요하면 Nx가 낫다.
실무에서 자주 밟는 함정
- outputs 누락: 캐시 히트인데 산출물이 없다면 대개 이 문제다. 태스크가 만드는 모든 경로를 명시한다.
- env 누락: 빌드에 영향 주는 환경변수는 반드시 선언한다. 안 그러면 오염된 캐시가 재사용된다.
- 비결정적 빌드: 타임스탬프·랜덤 값이 산출물에 섞이면 캐시 신뢰도가 떨어진다. 재현 가능한 빌드를 우선한다.
- shallow clone: CI에서 affected/filter가 오작동하면 fetch-depth부터 확인한다.
정리하면, 태스크 그래프로 순서를, 콘텐츠 해시로 중복 실행을, 원격 캐시로 팀 전체의 반복을 없앤다. 도구 선택보다 outputs·env를 정확히 선언하는 규율이 캐시 효과를 좌우한다.