Turborepo 운영 고도화: remote cache, CI 최적화, shared package 전략
이 글은 Turborepo 시리즈 3편입니다.
- 1편: Turborepo를 알아보자: 프론트엔드 모노레포 실무 적용과 운영 가이드
- 2편: pnpm + Turborepo + Next.js: 프론트엔드 모노레포 시작하기
- 4편: Turborepo로 운영하며 자주 만나는 문제들: cache miss, shared package 안티패턴, 경계 붕괴
앞선 글에서 Turborepo가 왜 필요한지, 그리고 pnpm + Turborepo + Next.js 조합으로 어떻게 시작할 수 있는지 정리했다면, 이번에는 그 다음 단계인 운영 고도화를 봐야 합니다.
실무에서 진짜 차이가 나는 지점은 보통 여기입니다.
- remote cache를 붙였는데 왜 cache hit가 기대만큼 안 나는가
- CI는 한 줄로 돌릴지, job을 나눌지, filter를 쓸지
- shared package를 늘릴수록 왜 오히려 build 범위가 넓어지는가
- 공통 설정 패키지가 왜 레포 전체를 흔드는가
즉, Turborepo 운영의 핵심은 "더 빠르게 돌린다"보다, 무엇이 왜 다시 실행되는지 팀이 설명할 수 있는 상태를 만드는 것에 더 가깝습니다.
한눈에 보면
이번 글의 핵심은 세 가지입니다.
remote cache: 같은 task를 팀과 CI가 반복하지 않게 만드는 것CI 최적화: 전체 실행과 부분 실행의 경계를 나누는 것shared package 전략: 공통화와 변경 영향 범위 사이의 균형을 잡는 것
조금 더 자세히 보면 아래처럼 볼 수 있습니다.
| 운영 포인트 | 무엇을 봐야 하나 |
|---|---|
| remote cache | 같은 입력이면 같은 결과를 안전하게 재사용하는가 |
| CI | 어떤 job을 병렬화하고 어떤 job은 통합할 것인가 |
| shared package | 공통화가 생산성인지, 영향 범위 확장인지 |
turbo.json |
inputs/outputs가 실제 레포 구조를 잘 반영하는가 |
| DX | 개발자가 루트 명령만으로도 흐름을 이해할 수 있는가 |
빠르게 정리하면, Turborepo 운영은 캐시를 붙이는 일보다 task 경계와 package 경계를 명확히 하는 일에 더 가깝습니다.
1. remote cache는 왜 중요한가?
로컬에서만 개발할 때는 Turborepo의 캐시 효과가 제한적으로 느껴질 수 있습니다. 하지만 팀과 CI를 같이 보면 상황이 달라집니다.
예를 들어 같은 PR에서 아래 일이 반복될 수 있습니다.
- 개발자 로컬에서
web build - CI에서
web build - preview 배포 단계에서 다시
web build
입력이 같은데 같은 계산을 여러 번 하는 셈입니다.
이때 remote cache를 붙이면 팀 전체가 task 결과를 공유할 수 있습니다.
2. remote cache는 어떻게 붙이게 될까?
실무에서는 보통 아래 두 흐름 중 하나를 보게 됩니다.
1. 로그인 기반 연결
turbo login
turbo link이 방식은 비교적 빠르게 시작하기 좋습니다.
2. 환경 변수 기반 연결
CI나 팀 환경에서는 보통 환경 변수로 다루게 됩니다.
TURBO_TEAM=repo
TURBO_TOKEN=***필요하면 custom remote cache 서버를 기준으로 API URL을 지정하는 경우도 있습니다.
TURBO_API=https://turbo-cache.example.com
TURBO_TEAM=repo
TURBO_TOKEN=***여기서 중요한 것은 설정 방법보다 아래입니다.
- 어떤 환경에서 같은 캐시를 공유할지
- preview와 production이 같은 캐시를 써도 되는지
- 민감한 환경 차이를 inputs에 제대로 반영했는지
즉, remote cache는 단순히 붙이는 기능이 아니라 어떤 결과를 같은 결과로 볼 것인지에 대한 운영 판단이 같이 따라옵니다.
3. cache hit가 기대보다 안 나는 이유는 무엇일까?
실무에서는 "remote cache를 붙였는데 왜 별로 안 빨라졌지?"가 자주 나옵니다.
대부분 원인은 아래 중 하나입니다.
1. 입력이 너무 넓다
예를 들어 공통 설정 파일을 너무 많이 포함하면, 작은 수정에도 캐시가 자주 깨집니다.
{
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", "../../**/*"]
}
}
}이런 식의 넓은 입력은 운영하기 편해 보이지만, 실제로는 cache miss를 많이 만듭니다.
2. 입력이 너무 좁다
반대로 환경 변수나 설정 파일을 빠뜨리면 캐시는 잘 맞는 것처럼 보여도 결과가 불안정해집니다.
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**"]
}
}
}이 설정에 .env.production이나 next.config.ts 변경이 반영되지 않으면, 빠르기보다 위험한 캐시가 됩니다.
3. 공통 패키지 변경 범위가 너무 넓다
packages/ui, packages/config-eslint, packages/config-typescript 같은 패키지는 많은 앱이 의존하는 경우가 많습니다.
즉, 공통화가 잘 되어 있을수록 생산성은 좋아지지만, 동시에 cache invalidation 범위는 넓어질 수 있습니다.
4. turbo.json은 운영 단계에서 어떻게 달라질까?
초기 세팅에서는 단순한 설정으로 시작해도 되지만, 운영 단계에서는 입력과 출력을 조금 더 구체적으로 잡게 됩니다.
예를 들어:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false,
"persistent": true
},
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env.production"],
"outputs": [".next/**", "dist/**"]
},
"lint": {
"outputs": []
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}여기서 읽는 포인트는 이렇습니다.
build.env.production을 입력에 포함해 환경 차이를 반영
lint- 보통 산출물이 없으므로
outputs를 비워두는 구성이 흔함
- 보통 산출물이 없으므로
test- coverage 결과를 output으로 둘지 여부는 팀 정책에 따라 다름
즉, 운영 단계의 turbo.json은 단순 실행 설정이 아니라 캐시의 신뢰도와 실행 범위를 조절하는 파일이 됩니다.
5. CI는 한 번에 돌릴까, job을 나눌까?
처음에는 보통 한 줄로 시작합니다.
- run: pnpm turbo run lint test build이 방식의 장점은 단순함입니다.
- 설정이 간단하고
- 디버깅 포인트가 적고
- 온보딩이 쉽습니다
하지만 레포가 커지면 job을 나누는 쪽이 더 낫기도 합니다.
예시: 단계 분리
- run: pnpm turbo run lint
- run: pnpm turbo run test
- run: pnpm turbo run build이 방식은 실패 지점을 더 빨리 찾기 좋습니다.
예시: 앱 기준 분리
- run: pnpm turbo run build --filter=web
- run: pnpm turbo run build --filter=admin이 방식은 앱별 배포 파이프라인이 분리되어 있을 때 유리합니다.
예시: 공통 패키지 검증 분리
- run: pnpm turbo run lint test --filter=@repo/ui즉, CI 전략은 정답이 하나가 아니라, 실패를 어디서 끊어 보고 싶은가와 배포 단위를 어떻게 가져가는가에 따라 달라집니다.
6. Next.js 모노레포에서는 무엇을 더 조심해야 할까?
Next.js 앱은 일반적인 라이브러리 build보다 영향 요소가 많습니다.
.next/**출력물- 환경 변수
- App Router 관련 빌드 결과
- shared package transpilation
예를 들어 packages/ui가 바뀌면 아래처럼 여러 앱이 영향을 받을 수 있습니다.
packages/ui
├─> apps/web
├─> apps/admin
└─> apps/docs이럴 때 중요한 것은 "왜 다시 빌드됐지?"를 감으로 보지 않는 것입니다.
turbo run build --graph이 명령으로 실제 task graph를 보고:
- 어떤 패키지 변경이 전파됐는지
- 왜
web build가 다시 돌았는지 - 왜
docs까지 같이 포함됐는지
를 설명할 수 있어야 합니다.
즉, Next.js 모노레포에서는 캐시보다 먼저 graph를 설명할 수 있는 상태가 중요합니다.
7. shared package는 어디까지 쪼개는 게 좋을까?
이 질문은 실무에서 정말 자주 나옵니다.
처음에는 공통화가 좋아 보이기 때문에 아래처럼 계속 쪼개기 쉽습니다.
packages/
ui/
auth/
api/
hooks/
utils/
common/
shared/문제는 이렇게 되면 package 수는 많아지는데, 경계는 오히려 흐려질 수 있다는 점입니다.
좋지 않은 신호
utils안에 도메인 로직이 들어감shared가 사실상 아무거나 들어가는 폴더가 됨common과shared차이를 팀이 설명하지 못함- 공통 패키지 하나 바꾸면 모든 앱이 영향을 받음
비교적 안정적인 시작점
packages/
ui/
api/
config-eslint/
config-typescript/즉, 초반에는 공통성이 명확한 패키지부터 시작하고, 정말 분리 필요성이 생길 때만 늘리는 편이 낫습니다.
8. 공통화는 왜 때로는 비용이 될까?
공통화는 보통 좋은 일처럼 보입니다. 하지만 Turborepo 운영에서는 항상 이 질문을 같이 해야 합니다.
- 이 패키지를 분리하면 재사용이 늘어나는가
- 아니면 단지 dependency fan-out만 넓어지는가
예를 들어 config-typescript 패키지는 편합니다.
{
"extends": "@repo/config-typescript/base.json"
}하지만 이 파일을 바꾸면 여러 앱의 타입 체크와 build가 같이 영향을 받을 수 있습니다.
즉, 공통 패키지는 편의와 함께 변경 반경도 가져옵니다.
이 관점은 ui, auth, api에도 똑같이 적용됩니다.
9. DX는 언제 좋아지고 언제 나빠질까?
Turborepo는 보통 초반 DX가 좋은 편입니다.
{
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test"
}
}루트 명령만 알아도 전체 흐름이 보이기 때문입니다.
하지만 운영이 복잡해지면 DX는 아래 지점에서 나빠질 수 있습니다.
- 왜 cache miss가 났는지 설명이 안 될 때
- 공통 패키지 의존성이 너무 넓을 때
turbo.jsoninputs/outputs가 실제 구조를 반영하지 못할 때- CI job 분리가 레포 구조와 맞지 않을 때
즉, Turborepo의 DX는 도구 자체보다 레포 구조와 task 관계가 얼마나 명확한가에 더 많이 좌우됩니다.
10. 운영 단계에서 자주 하는 실수는 무엇일까?
1. remote cache를 너무 빨리 붙인다
remote cache는 강력하지만, task 입력/출력이 부정확한 상태에서 먼저 붙이면 문제를 더 감추기도 합니다.
초기에는:
- local cache로 먼저 안정성을 확인하고
- 어떤 task가 자주 반복되는지 보고
- 그다음 remote cache를 붙이는 편이 더 안전합니다
2. CI를 너무 일찍 잘게 쪼갠다
job을 세분화하면 좋아 보이지만, 실제로는:
- 로그가 분산되고
- 디버깅이 어려워지고
- cache 전략도 복잡해질 수 있습니다
처음에는 단순하게 가고, 병목이 명확할 때만 나누는 편이 낫습니다.
3. shared package를 재사용성만 보고 늘린다
재사용 가능하다는 이유만으로 package를 늘리면, 장기적으로는 dependency fan-out과 cache invalidation 범위만 커질 수 있습니다.
4. graph를 안 보고 감으로 운영한다
레포가 커질수록 중요한 것은 사람 감이 아니라 graph입니다.
turbo run build --graph이걸 자주 보고 "왜 이 task가 도는가"를 설명할 수 있어야 운영이 안정됩니다.
정리하면
Turborepo 운영 고도화의 핵심은 도구를 더 많이 붙이는 것이 아닙니다. 오히려 아래를 명확하게 만드는 쪽에 가깝습니다.
- 어떤 task를 캐시할 것인가
- 어떤 입력이 결과를 바꾸는가
- CI를 어디서 나눌 것인가
- shared package를 어디까지 공통화할 것인가
실무 기준으로 정리하면:
- remote cache는 반복 작업이 충분히 많을 때 의미가 커지고
- CI 최적화는 병목이 보일 때부터 나누는 편이 낫고
- shared package 전략은 재사용성보다 영향 범위를 같이 봐야 합니다
중요한 것은 Turborepo를 "빠른 도구"로만 보는 것이 아니라, 레포의 실행 구조를 팀이 설명할 수 있게 만드는 도구로 보는 것입니다. 그 상태가 되면 CI와 캐시, 공통 패키지 운영도 훨씬 안정적으로 굴러갑니다.
다음 글에서 이어서 보기
여기까지는 remote cache, CI, shared package 전략을 운영 관점에서 정리했습니다. 다음 글에서는 한 단계 더 들어가서, 실제 운영 중 자주 만나는 cache miss, shared package 안티패턴, 경계 붕괴 문제를 원인과 대응 방식 중심으로 정리합니다.
