Turborepo를 알아보자: 프론트엔드 모노레포 실무 적용과 운영 가이드
이 글은 Turborepo 시리즈 1편입니다.
- 2편: pnpm + Turborepo + Next.js: 프론트엔드 모노레포 시작하기
- 3편: Turborepo 운영 고도화: remote cache, CI 최적화, shared package 전략
- 4편: Turborepo로 운영하며 자주 만나는 문제들: cache miss, shared package 안티패턴, 경계 붕괴
프론트엔드 모노레포를 이야기할 때 Turborepo는 자주 등장합니다. 특히 Next.js, React, pnpm workspace 조합에서는 비교적 자연스럽게 같이 언급됩니다.
이유는 단순합니다. 앱과 패키지가 조금만 늘어나도 아래 문제가 빠르게 드러나기 때문입니다.
- 앱마다 같은 build, lint, test를 반복 실행하게 되고
- 공통 패키지 하나를 바꿨는데 전체 앱을 다시 돌리게 되며
- CI 시간이 길어지고
- 어떤 변경이 어디에 영향을 주는지 감으로만 판단하게 됩니다
이때 Turborepo는 레포 구조를 완전히 바꾸는 도구라기보다, 이미 있는 workspace 위에 태스크 실행과 캐시 레이어를 얹는 도구로 보는 편이 맞습니다.
이 글에서는 Turborepo를 기능 소개보다 실무에서 왜 도입하는지, 어떤 구조로 쓰는지, 운영하면서 무엇을 보게 되는지에 초점을 맞춰 정리해보겠습니다. 특히 Next.js 기반 프론트엔드 모노레포를 기준으로 설명하겠습니다.
한눈에 보면
먼저 짧게 정리하면 이렇습니다.
Turborepo는 모노레포 자체를 만들어주는 도구라기보다, 모노레포의 실행 비용을 줄여주는 도구에 가깝습니다.- package manager workspace와 함께 쓸 때 가장 자연스럽습니다.
- 핵심은
task graph,caching,filter,remote cache입니다. - 프론트엔드에서는 특히
Next.js앱 여러 개와 shared package를 함께 운영할 때 체감이 큽니다.
조금 더 자세히 보면 아래처럼 볼 수 있습니다.
| 항목 | Turborepo |
|---|---|
| 기본 역할 | 모노레포 task orchestrator |
| 잘하는 일 | build/lint/test/dev 실행 최적화, 캐시, 병렬 실행 |
| 핵심 개념 | package graph, task graph, inputs/outputs, filter |
| 잘 맞는 레포 | apps/* + packages/* 구조의 프론트엔드 모노레포 |
| 특히 체감되는 곳 | CI, shared package 변경, Next.js 앱 여러 개 운영 |
| 주의할 점 | 구조 규칙 자체를 강하게 강제해주지는 않음 |
빠르게 정리하면, workspace는 package manager가 관리하고, Turborepo는 그 위에서 작업 실행을 더 똑똑하게 해주는 도구라고 이해하면 됩니다.
먼저 알아야 할 것: Turborepo는 무엇을 해결할까?
많은 팀이 모노레포를 시작할 때는 pnpm workspace만으로도 충분합니다.
예를 들어 아래 정도 구조는 workspace만으로도 잘 굴러갑니다.
repo/
apps/
web/
admin/
packages/
ui/
api/
config-eslint/
config-typescript/하지만 프로젝트가 조금만 커져도 문제가 생깁니다.
packages/ui만 바꿨는데web,admin을 다 다시 빌드하게 되고- 앱별로
pnpm --filter명령을 사람이 직접 기억해야 하며 - 공통 패키지 변경과 앱 변경이 어떤 순서로 실행되어야 하는지 관리가 필요해집니다
즉, workspace는 패키지를 연결해주지만, 무엇을 어떤 순서로 얼마나 다시 실행할지까지는 충분히 해결해주지 않습니다.
그 지점부터 Turborepo가 등장합니다.
실무에서 자주 보는 디렉토리 구조
프론트엔드 팀에서 Turborepo를 붙일 때 자주 보게 되는 구조는 보통 아래와 같습니다.
repo/
apps/
web/ # 사용자 서비스 (Next.js)
admin/ # 운영자 서비스 (Next.js)
docs/ # 문서/마케팅 사이트
packages/
ui/ # 공통 UI 컴포넌트
auth/ # 인증 로직
api/ # API client
utils/ # 공통 유틸
config-eslint/ # ESLint 공통 설정
config-typescript/ # TypeScript 공통 설정
package.json
pnpm-workspace.yaml
turbo.json이 구조에서 중요한 것은 앱과 패키지를 나누는 것 자체보다, 공통 패키지 변경이 어떤 앱 task를 다시 돌리게 만드는가입니다.
예를 들어:
apps/web -----> packages/ui -----> packages/utils
apps/admin ---> packages/ui -----> packages/utils
apps/admin ---> packages/auth ---> packages/api
apps/docs ----> packages/ui이 관계가 package graph의 기본 감각입니다. Turborepo는 이 관계 위에서 task를 실행합니다.
Turborepo의 핵심 개념은 네 가지다
1. package graph
이건 package manager가 이미 알고 있는 내부 패키지 의존성 관계입니다.
apps/web -----> packages/ui
apps/admin ---> packages/ui
packages/ui --> packages/utils즉, web이 ui에 의존하고, ui가 다시 utils에 의존하면 build 순서도 그 관계를 따라가야 합니다.
2. task graph
Turborepo는 패키지 의존성만 보는 것이 아니라, 그 위에 build, lint, test, dev 같은 task를 올려서 graph를 만듭니다.
예를 들어 web#build는 보통 아래 task들에 간접적으로 연결됩니다.
web#build
-> ui#build
-> utils#build이 감각이 중요한 이유는, 실무에서 궁금한 것은 보통 "어떤 패키지가 연결돼 있나?"보다 "이 변경 때문에 실제로 어떤 task가 다시 도는가?" 이기 때문입니다.
3. caching
Turborepo의 핵심 체감 포인트는 캐시입니다. 입력이 같으면 이전 task 결과를 재사용할 수 있습니다.
즉:
- 코드가 안 바뀌었고
- 설정도 안 바뀌었고
- 환경도 동일하다면
굳이 같은 build를 다시 하지 않도록 해줍니다.
4. filter
모노레포에서는 전체를 매번 다 돌리지 않는 것도 중요합니다.
예를 들어:
turbo run build --filter=web
turbo run test --filter=@repo/ui이런 식으로 특정 앱이나 패키지 기준으로 task 범위를 줄일 수 있습니다.
실제로는 turbo.json을 어떻게 보게 될까?
가장 많이 보게 되는 파일은 turbo.json입니다.
예를 들어 Next.js 기반 프론트엔드 모노레포라면 시작점은 보통 이런 식입니다.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false,
"persistent": true
},
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"lint": {},
"test": {
"dependsOn": ["^build"]
}
}
}여기서 읽는 포인트는 이렇습니다.
dev- 개발 서버는 매번 실행 상태가 달라지므로 보통 캐시하지 않습니다.
build- 상위 의존 패키지의
build가 먼저 끝나야 합니다. - Next.js 앱은
.next/**, 라이브러리는dist/**같은 산출물을 캐시 대상으로 둡니다.
- 상위 의존 패키지의
test- 빌드된 출력이 필요한 테스트라면
^build에 의존시키기도 합니다.
- 빌드된 출력이 필요한 테스트라면
즉, turbo.json은 단순 설정 파일이라기보다 레포에서 어떤 작업이 어떤 순서로 실행되어야 하는지 적는 운영 문서에 가깝습니다.
Next.js 모노레포에서 왜 체감이 큰가?
프론트엔드에서 Turborepo가 특히 많이 이야기되는 이유는 Next.js와의 조합 때문입니다.
예를 들어:
apps/webapps/adminpackages/uipackages/auth
구조를 생각해보면, packages/ui만 바뀌어도 web과 admin의 build 결과는 영향을 받을 수 있습니다.
이때 Turborepo를 쓰지 않으면 CI에서 보통 이런 식으로 흘러가기 쉽습니다.
pnpm --filter web build
pnpm --filter admin build
pnpm --filter web lint
pnpm --filter admin lint
pnpm --filter web test
pnpm --filter admin test이 방식은 나쁘지 않지만, 레포가 커질수록 사람이 어느 범위를 돌려야 하는지 직접 관리하게 됩니다.
반면 Turborepo를 붙이면 아래처럼 시작할 수 있습니다.
turbo run build lint test그 뒤에는 package graph와 task graph를 따라 필요한 task만 실행하고, 같은 입력이면 캐시를 재사용합니다.
package graph를 어떻게 이해하면 좋을까?
실무에서 graph는 이론보다 디버깅 도구에 가깝습니다.
예를 들어 이런 구조가 있다고 가정해보겠습니다.
apps/web -----> packages/ui -----> packages/utils
apps/admin ---> packages/ui -----> packages/utils
apps/admin ---> packages/auth ---> packages/api여기서 packages/utils가 바뀌면 실제 영향은 꽤 넓어집니다.
changed: packages/utils
packages/utils
└─> packages/ui
├─> apps/web
├─> apps/admin
└─> apps/docs이럴 때 팀이 궁금한 것은 보통 이겁니다.
- 왜
web이 다시 빌드됐는가 - 왜
docs까지 영향 받는가 - 왜
admin테스트가 다시 돌았는가
Turborepo는 이걸 package graph와 task graph 기준으로 설명하게 해줍니다.
turbo run build --graph이 명령은 단순히 시각화용이 아니라, 왜 특정 task가 포함됐는지 확인하는 실무 도구로 보는 편이 좋습니다.
캐시는 어디서 진짜 차이가 날까?
로컬 개발보다 CI에서 차이가 훨씬 크게 느껴집니다.
예를 들어:
web앱은 안 바뀌었고admin만 수정했고- 공통 패키지도 안 바뀌었다면
이상적으로는 web build를 다시 할 필요가 없습니다.
Turborepo는 이걸 캐시로 줄여줍니다.
다만 여기서 중요한 것은 캐시 자체보다 입력과 출력 정의를 얼마나 정확히 잡느냐입니다.
예를 들어:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env.production"],
"outputs": [".next/**", "dist/**"]
}
}
}여기서 읽는 포인트는:
.env.production이 바뀌면 build 캐시는 다시 계산되어야 하고.next/**가 build 결과물이며- 상위 패키지 build 결과도 같이 고려해야 한다는 점입니다
이 부분이 잘못 잡히면 캐시가 "빠르다"보다 "불안하다"가 됩니다. 실무에서 중요한 것은 cache hit 비율보다 재현 가능한 캐시입니다.
filter는 언제 유용할까?
레포 전체를 돌리는 것만이 답은 아닙니다. 로컬에서는 범위를 줄이는 것이 더 중요할 때가 많습니다.
예를 들어:
turbo run dev --filter=web
turbo run lint --filter=admin
turbo run test --filter=@repo/ui이런 명령은 아래 상황에서 편합니다.
- 특정 앱 하나만 개발할 때
- shared package만 빠르게 검증할 때
- CI에서 특정 job만 잘라서 돌릴 때
즉, Turborepo는 "전체 최적화"뿐 아니라 부분 실행을 명확하게 가져가는 도구로도 유용합니다.
remote cache는 언제부터 중요해질까?
로컬에서는 체감이 제한적일 수 있어도, 팀과 CI를 붙이면 remote cache의 의미가 커집니다.
예를 들어:
- 같은 PR을 개발자 로컬에서 한 번 빌드했고
- CI에서도 같은 build를 다시 하며
- preview 환경에서 또 비슷한 작업을 반복한다면
중복 계산 비용이 꽤 큽니다.
이때 remote cache가 있으면 팀 전체가 같은 task 결과를 공유할 수 있습니다.
실무 관점에서 중요한 것은 기능 자체보다 아래입니다.
- 캐시 키가 환경 차이를 잘 반영하는가
- 민감한 환경 변수가 섞여 있지는 않은가
- 브랜치 전략 때문에 cache miss가 과도하게 발생하지 않는가
- CI가 정말 cache hit를 체감할 정도로 반복 작업이 많은가
즉, remote cache는 단순히 "붙이면 빨라진다"가 아니라, CI와 팀 작업 패턴이 반복적인지를 기준으로 보는 편이 더 정확합니다.
개발자 경험(DX)은 어디서 좋아질까?
Turborepo의 DX는 비교적 즉각적인 편입니다.
turbo run build
turbo run lint
turbo run test
turbo run dev --filter=web배워야 할 개념이 아주 많지는 않고, 기존 package.json scripts 위에 얹는 감각이라 온보딩도 비교적 편합니다.
예를 들어 루트 package.json은 보통 이런 식으로 정리됩니다.
{
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test"
}
}이 방식의 장점은 팀원이 루트 명령만 알아도 전체 흐름을 이해하기 쉽다는 점입니다.
다만 DX가 항상 좋아지는 것은 아닙니다. 아래는 팀이 직접 관리해야 합니다.
- shared package 경계를 어떻게 나눌지
- 어떤 task를 cache 대상으로 둘지
.env같은 입력을 어디까지 캐시에 포함할지- 앱이 너무 많아졌을 때 구조 규칙을 어떻게 유지할지
즉, Turborepo는 실행 DX는 좋아지게 해주지만, 구조 DX까지 자동으로 해결해주지는 않습니다.
실무에서 운영하면서 자주 보게 되는 포인트
1. dev는 cache하지 않는 경우가 많다
개발 서버는 상태가 계속 바뀌고 watch 모드로 오래 살아있기 때문에, 보통 아래처럼 둡니다.
{
"tasks": {
"dev": {
"cache": false,
"persistent": true
}
}
}2. Next.js build 출력은 명확하게 잡아야 한다
Next.js 앱이면 .next/**, 라이브러리면 dist/**처럼 출력물을 명확하게 잡는 편이 좋습니다.
{
"tasks": {
"build": {
"outputs": [".next/**", "dist/**"]
}
}
}3. 설정 패키지 변경이 생각보다 크게 퍼진다
config-eslint, config-typescript, shared-ui 같은 패키지는 많은 앱에 붙어 있기 때문에, 작은 수정도 영향 범위가 넓습니다.
즉, 공통 패키지를 늘리는 것 자체가 생산성일 수도 있지만, 동시에 캐시 무효화 범위를 넓히는 선택일 수도 있습니다.
4. 사람 머리보다 graph를 신뢰하는 쪽이 낫다
레포가 커질수록 "이 정도는 영향 없겠지"라는 감이 자주 틀립니다.
이럴수록 중요한 것은:
- dependency를 명확히 선언하고
- task 출력을 명확히 적고
- graph 기준으로 실행되게 하는 것
입니다.
어떤 팀에 잘 맞을까?
Turborepo가 잘 맞는 경우
React/Next.js중심 프론트엔드 모노레포- 앱 여러 개와 shared package를 같이 운영하는 팀
- 빠른 도입과 빠른 CI 개선이 중요한 팀
- package manager workspace는 이미 쓰고 있는 팀
- 복잡한 운영 체계보다 단순한 실행 모델을 선호하는 팀
아직 과할 수 있는 경우
- 앱이 거의 하나뿐이고 shared package도 많지 않을 때
- CI 시간이 아직 큰 문제가 아닐 때
- workspace만으로도 충분히 운영 가능한 규모일 때
즉, Turborepo는 "모노레포라서 무조건 쓰는 도구"라기보다, 모노레포 운영 비용이 눈에 띄기 시작할 때 붙이는 도구로 보는 편이 맞습니다.
정리하면
Turborepo는 프론트엔드 모노레포에서 꽤 현실적인 선택지입니다. 특히 Next.js 앱 여러 개와 shared package를 같이 운영하는 환경에서는 체감이 빠른 편입니다.
핵심은 이것입니다.
- workspace는 package manager가 관리하고
- Turborepo는 그 위에서 task 실행을 최적화하고
- 캐시, graph, filter를 통해 중복 작업을 줄입니다
실무 관점에서 정리하면:
- 빠르게 붙이고 바로 체감하기 좋은 모노레포 도구
- Next.js/React 프론트엔드 레포와 궁합이 좋은 편
- 실행 속도와 CI 개선에는 강하지만, 구조 규칙 자체는 팀이 같이 관리해야 함
중요한 것은 Turborepo를 도입하는 것 자체가 아니라, 우리 레포에서 어떤 반복 비용을 줄이려는가입니다. 그 질문이 분명하면 Turborepo는 꽤 실용적인 답이 될 수 있습니다.
다음 글에서 이어서 보기
여기까지는 Turborepo가 무엇이고 왜 필요한지, 프론트엔드 모노레포에서 어떤 식으로 작동하는지 정리했습니다. 다음 글에서는 한 단계 더 나아가서, 실제로 pnpm + Turborepo + Next.js 조합으로 어떤 파일을 만들고 어떤 구조로 시작하면 좋은지 코드 중심으로 정리합니다.
