Turborepo 도입으로 빌드 시간과 CI 가독성 함께 개선하기
- 1.다중 OAuth 제공자 환경에서 DX와 UX 동시에 달성하기
- 2.천진난만 Go 성적계산기 도입 이야기
- 3.Turborepo 도입으로 빌드 시간과 CI 가독성 함께 개선하기← 현재
- 4.Zustand로 흩어진 모달 상태를 한곳에서 관리하기

서론
Hello, GSM 프론트엔드는 사용자용 페이지와 관리자 페이지, 그리고 공통 패키지를 하나의 저장소에서 관리하고 있습니다. 기존 pnpm workspace 기반 모노레포 구조는 코드와 의존성을 함께 관리하기에 적합했지만, 프로젝트가 커지며 빌드와 CI를 실행하는 경험에는 개선할 지점이 생겼습니다.
이번 글에서는 Turborepo를 도입한 이유, 마이그레이션 중 겪은 문제, 그리고 실제로 얻은 변화와 아쉬웠던 점을 정리합니다.
Turborepo 이해하기
모노레포란?
모노레포는 여러 애플리케이션과 라이브러리를 하나의 Git 저장소에서 관리하는 방식입니다. Hello, GSM처럼 사용자용 페이지와 관리자 페이지가 공통 코드와 타입을 공유하는 환경에서 특히 잘 맞습니다.
각 프로젝트를 별도 저장소로 분리하지 않아도 되므로 코드 재사용이 쉽고, 의존성 버전과 개발 환경을 일관되게 유지할 수 있습니다. 반면 저장소 규모가 커질수록 빌드와 테스트 범위를 관리하기 어려워질 수 있습니다.
Turborepo란?
Turborepo는 JavaScript·TypeScript 모노레포를 위한 빌드 시스템입니다. 패키지 간 의존 관계를 그래프로 파악하고, 필요한 작업만 실행하거나 이전 결과를 캐시에서 재사용합니다.
예를 들어 A가 B에, B가 C에 의존한다면 C, B, A 순으로 작업해야 합니다. Turborepo는 이 순서를 자동으로 관리하고, 입력과 결과가 같다면 이미 수행한 작업을 다시 실행하지 않습니다.
이번 작업의 목표는 기존 모노레포 구조를 바꾸는 것이 아니라, 그 위에서 빌드와 CI를 더 효율적으로 실행하는 것이었습니다.
마이그레이션을 결정한 이유
기존에도 concurrently로 여러 빌드 명령을 병렬 실행하고 있었습니다. 하지만 concurrently는 명령을 동시에 실행할 뿐, 패키지 간 의존 관계나 이전 실행 결과를 이해하지는 못합니다.
그 결과 모든 앱과 패키지의 로그가 한곳에 섞여 출력됐습니다. CI가 실패했을 때 어느 패키지에서 문제가 시작됐는지 파악하기 어려웠고, 변경되지 않은 패키지까지 반복해서 빌드했습니다.
필요했던 것은 단순 병렬 실행이 아니라 다음을 할 수 있는 빌드 시스템이었습니다.
- 변경된 패키지와 영향 범위만 실행할 것
- 패키지 간 의존 관계에 맞춰 작업 순서를 정할 것
- 이전 작업 결과를 캐시해 반복 실행을 줄일 것
- CI 로그를 작업 단위로 분리할 것
이 조건을 만족하는 도구로 Turborepo를 선택했습니다.
마이그레이션 과정에서 겪은 문제
템플릿 버전과 기존 프로젝트의 충돌
Turborepo의 Next.js 모노레포 템플릿은 Next.js 15, Tailwind CSS 4, ESLint 9를 기준으로 구성되어 있었습니다. 반면 기존 프로젝트는 Next.js 14, Tailwind CSS 3, ESLint 8을 사용하고 있었습니다.
| 도구 | 템플릿 | 기존 프로젝트 |
|---|---|---|
| Next.js | 15 | 14 |
| Tailwind CSS | 4 | 3 |
| ESLint | 9 | 8 |
처음에는 Next.js만 14로 유지하고, Tailwind CSS와 ESLint는 최신 메이저 버전으로 올리는 방향을 검토했습니다. 하지만 빌드 시스템 전환과 도구 체인의 메이저 업그레이드를 함께 진행하면서 문제가 연달아 발생했습니다.
먼저 eslint-config-next를 Next.js 14에 맞춰 낮추자 ESLint 9와 호환되지 않았습니다. 이미 ESLint 9 기준의 Flat Config 설정까지 적용한 상태였지만, 결국 ESLint를 8로 되돌리고 설정을 다시 구성해야 했습니다.
이후에는 ESLint가 플러그인을 찾지 못하는 문제가 생겼습니다. ESLint 8 이하는 실행 위치를 기준으로 node_modules를 탐색하는데, pnpm은 의존성을 깊은 구조로 관리합니다. 별도 설정이 없으면 린트 실행 위치에서 필요한 플러그인을 찾지 못합니다.
루트 .npmrc에 ESLint 관련 패키지의 호이스팅 규칙을 추가해 해결했습니다.
public-hoist-pattern[]=@typescript-eslint/*
public-hoist-pattern[]=eslint-plugin-*
public-hoist-pattern[]=eslint-config-*
public-hoist-pattern[]=@next/eslint-plugin-next
public-hoist-pattern[]=@cspell/eslint-plugin
public-hoist-pattern[]=eslint-import-resolver-typescript
Tailwind CSS도 같은 흐름을 밟았습니다. 기존 tailwind.config.js를 유지하기 위해 @config 지시어를 사용했지만, Next.js 14와 Tailwind CSS 4 조합에서 호환성 문제가 확인됐습니다. 안정성을 우선해 Tailwind CSS 역시 3으로 유지했습니다.
결국 이번 작업에서는 Turborepo 도입과 도구 체인의 메이저 업그레이드를 분리하기로 했습니다. 마이그레이션의 목적은 빌드 시스템 전환이었고, 버전 업그레이드까지 함께 해결하려 하면 원인과 영향 범위를 구분하기 어려웠습니다.
패키지 분리 후 드러난 순환 의존성
기존에는 사용자용·관리자용 애플리케이션이 공유하는 코드를 shared 패키지 하나에 모아두고 있었습니다. Turborepo의 “변경된 패키지만 다시 실행한다”는 장점을 더 활용하기 위해, 공통 코드를 역할별 패키지로 나누기로 했습니다.
- 유틸리티 함수는
utils - 타입 관련 코드는
types - API 관련 코드는
api - 그 외 공통 코드는 역할에 맞는 패키지
하지만 분리 후 빌드를 실행하자 순환 의존성이 드러났습니다. 한 패키지가 다른 패키지를 참조하고, 그 패키지가 다시 처음 패키지를 참조하는 구조였습니다. 기존에는 shared 패키지 내부에 함께 있던 코드라 눈에 띄지 않았던 의존 관계였습니다.
문제되는 코드를 적절한 위치로 옮겨 순환 의존성 자체는 해소할 수 있었습니다. 다만 패키지를 나눈 뒤에는 각 패키지의 책임과 참조 방향을 계속 관리해야 했습니다. 실제로 패키지끼리 서로 참조해야 하는 경우가 적지 않았고, 이를 피하기 위한 코드 이동과 구조 조정에도 비용이 들었습니다.
결과적으로 공통 코드를 하나의 shared 패키지에 묶어 한 번 빌드하는 비용보다, 세분화된 패키지 사이의 의존성을 관리하는 비용이 더 크게 느껴지기도 했습니다. 변경 범위를 줄이기 위한 패키지 분리가 항상 더 효율적인 것은 아니었습니다.
이 경험을 통해 패키지 분리는 단순히 캐시 효율을 높이기 위한 수단이 아니라, 서로 독립적인 책임과 의존 방향이 이미 성립한 영역에 적용해야 한다는 점을 배웠습니다. 경계가 충분히 분명하지 않다면, 하나의 공통 패키지로 관리하는 편이 오히려 더 단순하고 유지보수하기 좋은 선택일 수 있습니다.
결과
가장 눈에 띄는 변화는 빌드 시간이었습니다. Galaxy Book2 Pro 기준으로 로컬 빌드를 측정한 결과는 다음과 같습니다.
| 구분 | 단축률 |
|---|---|
| Cold 빌드 | 약 17.8% 단축 |
| Warm 빌드 | 약 96.6% 단축 |
특히 캐시를 활용하는 Warm 빌드에서 효과가 컸습니다. 변경이 없는 작업을 다시 수행하지 않으므로, 반복 실행이 많은 개발과 CI 환경에서 체감 차이가 분명했습니다.
개발 경험도 함께 개선됐습니다.
- 기존에는
pnpm client dev,pnpm admin dev처럼 각각 실행해야 했지만, 이제는dev명령 하나로 두 애플리케이션을 함께 실행할 수 있습니다. - GitHub Actions에서 실패가 발생했을 때 패키지별 로그를 구분해 확인할 수 있습니다. 모든 로그가 섞여 있던 기존 방식보다 원인 파악이 쉬워졌습니다.
마무리
이번 작업은 Turborepo를 추가하는 데서 끝나지 않았습니다. 빌드와 CI가 저장소 구조를 얼마나 잘 이해해야 하는지, 그리고 패키지 분리가 언제 유지보수성을 높이고 언제 관리 비용을 키우는지 함께 확인한 작업이었습니다.
기존 구조도 동작에는 문제가 없었습니다. 다만 애플리케이션과 공통 패키지가 늘어난 시점에서는, 변경된 부분만 실행하고 결과를 재사용하는 Turborepo의 구조가 빌드 시간과 CI 경험을 개선하는 데 분명한 효과가 있었습니다.
반면 캐시 효율만을 위해 공통 코드를 지나치게 작은 패키지로 나누는 것은 좋은 선택이 아닐 수 있습니다. 패키지 경계와 의존 방향이 충분히 명확한지를 먼저 확인한 뒤, 그때 필요한 만큼만 분리하는 것이 더 중요하다고 느꼈습니다.
마이그레이션 과정에서는 버전 호환성과 순환 의존성처럼 예상하지 못한 문제가 있었지만, 결과적으로 빌드 시간과 CI 가독성을 함께 개선할 수 있었습니다.