Turborepo로 운영하며 자주 만나는 문제들: cache miss, shared package 안티패턴, 경계 붕괴
이 글은 Turborepo 시리즈 4편입니다.
- 1편: Turborepo를 알아보자: 프론트엔드 모노레포 실무 적용과 운영 가이드
- 2편: pnpm + Turborepo + Next.js: 프론트엔드 모노레포 시작하기
- 3편: Turborepo 운영 고도화: remote cache, CI 최적화, shared package 전략
앞선 글들에서 Turborepo의 개념, 시작 방법, 운영 고도화까지 정리했다면, 결국 마지막에 남는 것은 실제 문제입니다.
실무에서 더 자주 나오는 질문은 보통 이런 식입니다.
- 왜 cache hit가 날 것 같은데 계속 miss가 나는가
- 왜 작은 설정 변경 하나가 레포 전체 build를 흔드는가
- 왜
shared패키지가 시간이 갈수록 잡동사니 폴더가 되는가 - 왜 어떤 앱은 분명히 영향이 없어 보이는데 같이 다시 실행되는가
즉, Turborepo 운영에서 중요한 것은 "도구를 잘 쓴다"보다, 문제가 생겼을 때 graph와 task 관점으로 원인을 설명할 수 있는가입니다.
이 글에서는 실제로 자주 만나는 문제를 증상 -> 흔한 원인 -> 확인 방법 -> 대응 방식 순서로 정리해보겠습니다.
먼저 한눈에 보면
운영 중 자주 만나는 문제는 대체로 네 가지로 모입니다.
| 문제 | 흔한 원인 |
|---|---|
| cache miss가 너무 많다 | inputs가 너무 넓거나, 환경 차이가 제대로 관리되지 않음 |
| 작은 수정에도 전체가 다시 돈다 | 공통 패키지 fan-out이 너무 넓음 |
| shared package가 계속 커진다 | 경계 없는 공통화 |
| 왜 다시 실행됐는지 설명이 안 된다 | graph를 보지 않고 감으로 운영함 |
빠르게 정리하면, Turborepo 문제의 대부분은 캐시 기능 자체보다 package 경계와 task 입력 정의에서 시작됩니다.
1. 문제: cache miss가 너무 자주 난다
증상
- 같은 브랜치에서 비슷한 작업을 반복하는데도 hit가 거의 안 남
- local cache는 그럭저럭 맞는데 CI에서는 계속 miss가 남
- remote cache를 붙였는데 체감이 별로 없음
흔한 원인
가장 흔한 원인은 inputs가 너무 넓거나, 반대로 중요한 입력이 누락된 경우입니다.
예를 들어 너무 넓은 입력:
{
"tasks": {
"build": {
"inputs": ["$TURBO_DEFAULT$", "../../**/*"]
}
}
}이런 구성은 편해 보이지만, 실제로는 레포 어딘가의 작은 수정만 있어도 캐시가 쉽게 깨집니다.
반대로 너무 좁은 입력:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**"]
}
}
}이 설정에 .env.production, next.config.ts, 공통 설정 변경이 빠져 있으면, 캐시는 맞는 것처럼 보여도 결과가 불안정할 수 있습니다.
어떻게 확인할까?
먼저 "왜 다시 실행됐는가"를 감으로 보지 말고 graph와 task 기준으로 확인해야 합니다.
turbo run build --graph그리고 팀 안에서는 아래 질문을 같이 보게 됩니다.
- 이 task의 입력에는 무엇이 들어가는가
- 설정 파일 변경이 반영되고 있는가
- 환경 변수 차이를 같은 결과로 보고 있지는 않은가
어떻게 대응할까?
운영 단계에서는 보통 아래처럼 조금 더 명시적으로 잡습니다.
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env.production", "next.config.ts"],
"outputs": [".next/**", "dist/**"]
}
}
}핵심은 이렇습니다.
- 입력은 너무 넓어도 안 되고
- 너무 좁아도 안 되며
- 결과를 바꾸는 파일만 명시적으로 잡는 편이 좋습니다
즉, cache miss 문제는 캐시 서버보다 입력 정의 문제인 경우가 많습니다.
2. 문제: shared package 하나 바꿨는데 전체 앱이 흔들린다
증상
packages/ui수정이web,admin,docs전체 build를 다시 돌림config-typescript수정이 lint, test, build를 전부 흔듦- 사소한 공통 설정 변경인데 CI 시간이 갑자기 길어짐
흔한 원인
공통 패키지의 dependency fan-out이 너무 넓은 경우가 많습니다.
예를 들어:
packages/ui
├─> apps/web
├─> apps/admin
└─> apps/docs
packages/config-typescript
├─> apps/web
├─> apps/admin
├─> apps/docs
└─> packages/ui이 구조에서는 작은 수정 하나도 영향 범위가 넓습니다.
어떻게 확인할까?
이런 문제는 보통 graph를 보면 바로 감이 옵니다.
changed: packages/config-typescript
affected
├─> apps/web
├─> apps/admin
├─> apps/docs
└─> packages/ui이럴 때 중요한 질문은 이것입니다.
- 이 공통 패키지가 정말 이렇게 많은 곳에 필요했는가
- 공통화가 생산성인지, 단순 fan-out인지
어떻게 대응할까?
공통 패키지는 "재사용 가능하다"보다 "공통화했을 때 영향 범위를 감당할 수 있는가"로 봐야 합니다.
예를 들어:
config-typescript- 정말 모든 앱이 같은 설정을 가져야 하는가
ui- 모든 앱이 같은 UI package를 공유해야 하는가
api- 서비스별 API client까지 한 패키지에 묶는 것이 맞는가
즉, shared package는 편의와 함께 변경 반경을 가져옵니다.
3. 문제: shared, common, utils 패키지가 점점 비대해진다
증상
packages/shared안에 UI, hook, domain util, API helper가 다 들어감utils가 사실상 공용 쓰레기통이 됨- 신규 인력이
common과shared차이를 설명하지 못함
흔한 원인
처음에는 공통화가 좋아 보여서 아래처럼 패키지를 만들기 쉽습니다.
packages/
shared/
common/
utils/
hooks/
lib/문제는 이름이 추상적일수록 경계가 약해진다는 점입니다.
어떻게 확인할까?
아래 질문을 던져보면 금방 드러납니다.
- 이 패키지 이름만 보고 책임이 설명되는가
- 다른 사람이 들어왔을 때 어디에 코드를 넣어야 할지 바로 알 수 있는가
- 이 패키지 안에 도메인 코드와 범용 코드가 섞여 있지는 않은가
어떻게 대응할까?
초기에는 아래처럼 공통성이 명확한 패키지만 두는 편이 더 안정적입니다.
packages/
ui/
api/
config-eslint/
config-typescript/즉, 추상적인 공통 패키지보다 책임이 드러나는 패키지 이름이 더 중요합니다.
4. 문제: 왜 다시 실행됐는지 설명이 안 된다
증상
- "이건 안 돌아도 될 것 같은데 왜 다시 돌지?"가 반복됨
- 특정 앱만 바뀌었다고 생각했는데 다른 앱 build도 같이 실행됨
- 캐시가 맞는지 틀리는지 결과만 보고 추측하게 됨
흔한 원인
이 경우는 대부분 graph를 충분히 보지 않고 감으로 운영하기 때문입니다.
예를 들어:
turbo run build만 계속 쓰고, 왜 특정 task가 포함됐는지는 확인하지 않는 상태입니다.
어떻게 확인할까?
이럴 때는 아래 명령을 자주 보는 습관이 필요합니다.
turbo run build --graph그리고 아래 질문을 바로 답할 수 있어야 합니다.
- 어떤 package dependency 때문에 이 task가 포함됐는가
- 어떤 상위 build가 선행되어야 했는가
- 어떤 변경이 이 task를 invalidation했는가
어떻게 대응할까?
운영이 안정된 팀은 보통 "왜 이 task가 돌았는지"를 사람 감이 아니라 graph와 설정으로 설명합니다.
즉:
- 의존성 선언을 명확히 하고
turbo.jsoninputs/outputs를 정리하고- 설명이 안 되면 graph를 먼저 봅니다
5. 문제: CI를 쪼갰는데 더 복잡해진다
증상
- job은 많아졌는데 디버깅이 더 어려워짐
- cache 전략이 job마다 다르게 흩어짐
- 로그가 분산돼서 문제를 찾기 힘듦
흔한 원인
병목이 명확하지 않은 상태에서 job을 너무 빨리 나누는 경우가 많습니다.
예를 들어:
- run: pnpm turbo run lint --filter=web
- run: pnpm turbo run lint --filter=admin
- run: pnpm turbo run test --filter=web
- run: pnpm turbo run test --filter=admin
- run: pnpm turbo run build --filter=web
- run: pnpm turbo run build --filter=admin이런 구성은 세밀해 보이지만, 실제로는 레포 규모에 비해 과할 수 있습니다.
어떻게 확인할까?
먼저 질문해야 하는 것은 아래입니다.
- 지금 진짜 병목은 설치인가, build인가, test인가
- 앱별 배포가 실제로 분리되어 있는가
- 실패 지점을 더 빨리 찾는 것이 목적인가, 전체 시간을 줄이는 것이 목적인가
어떻게 대응할까?
초기에는 보통 단순하게 가는 편이 낫습니다.
- run: pnpm turbo run lint test build그다음 병목이 보이면:
- lint/test/build 단계 분리
- 앱 기준 분리
- shared package 검증 분리
순서로 조금씩 나누는 편이 현실적입니다.
6. 문제: 공통 설정 패키지가 레포 전체를 흔든다
증상
config-eslint를 조금 바꿨는데 lint 전체가 흔들림config-typescript수정이 build까지 넓게 영향 줌- 작은 설정 수정인데 CI 시간이 크게 증가함
흔한 원인
공통 설정은 재사용성이 높아서 좋아 보이지만, 동시에 거의 모든 프로젝트의 입력이 되기 쉽습니다.
예를 들어:
{
"extends": "@repo/config-typescript/base.json"
}이 패턴 자체는 자연스럽지만, 이 설정을 바꾸면 레포 전체의 타입 체크와 build 범위가 같이 흔들릴 수 있습니다.
어떻게 대응할까?
설정 패키지는 가능하면:
- 정말 자주 공통으로 유지해야 하는 것만 넣고
- 앱별 차이는 앱 안에서 흡수하며
- 변경이 잦은 설정은 공통 패키지로 너무 빨리 올리지 않는 편
이 더 안정적입니다.
정리하면
Turborepo 운영 중 자주 만나는 문제는 대부분 기능 부족보다 경계와 입력 정의가 불분명한 상태에서 나옵니다.
실무 기준으로 다시 정리하면:
- cache miss가 많다면 inputs를 먼저 보아야 하고
- shared package가 비대해지면 공통화 기준을 다시 보아야 하며
- 왜 다시 실행됐는지 설명이 안 되면 graph를 먼저 보아야 하고
- CI가 복잡해졌다면 병목이 무엇인지부터 다시 확인해야 합니다
중요한 것은 Turborepo를 빠른 도구로만 보는 것이 아니라, 레포의 실행 구조와 변경 반경을 설명하게 만드는 도구로 보는 것입니다. 이 관점이 잡히면 cache miss, fan-out, 경계 붕괴 같은 문제도 훨씬 빨리 정리할 수 있습니다.
