우선 글 자체를 굉장히 오랜만에 작성하다 보니, 글의 퀄리티가 다소 낮을 수 있는 점 양해 부탁드립니다.
약 한 달 전쯤, 작년에 개발부터 유지보수, 실제 운영까지 진행했던 프로젝트인 Hello, GSM의 프론트엔드를 단순 pnpm workspace 기반 모노레포에서 Turborepo 기반 모노레포로 마이그레이션했다.
이번 글에서는 마이그레이션을 진행하게 된 이유(Turborepo의 장점)와, 진행 과정에서 겪었던 이슈(삽질), 그리고 개선된 점들을 정리해보고자 한다.
본격적인 내용에 들어가기 전에, 모노레포와 Turborepo가 무엇인지 간단히 짚고 넘어가자.
모노레포는 두 개 이상의 프로젝트, 라이브러리, 혹은 애플리케이션 코드를 하나의 Git 저장소에서 관리하는 개발 전략이다.
여러 프로젝트가 하나의 저장소에 위치하기 때문에 코드 공유가 쉽고, 의존성 관리와 개발 경험을 일관되게 유지할 수 있다. 특히 사용자 페이지와 어드민 페이지처럼 밀접하게 연결된 프로젝트를 다룰 때 효과적이다.
| 내용 | |
|---|---|
| 장점 | 코드 재사용 용이, 의존성 버전 통합 관리, 일관된 CI/CD 파이프라인 구축 |
| 단점 | 저장소 규모가 커질수록 빌드·테스트 속도 저하, Turborepo·Nx 등 추가 도구 필요 |
각 프로젝트를 별도의 저장소로 관리하는 멀티레포(Multi-repo) 방식과 대비되는 개념이며, 여러 서비스가 공통 라이브러리를 공유하거나 통합된 빌드·배포 환경이 필요한 경우에 적합하다.
Turborepo는 Vercel에서 개발한 JavaScript/TypeScript 기반의 고성능 모노레포 빌드 시스템이다.
핵심은 캐싱(Caching) 과 병렬 처리를 통해 빌드 및 테스트 속도를 크게 향상시키는 데 있다.
주요 장점
turbo.json으로 작업 순서(파이프라인)를 정의하고 병렬 처리를 수행할 수 있다.모노레포와 Turborepo가 무엇인지 간단히 알아봤으니, 이제 본론으로 넘어가자.
마이그레이션을 진행한 이유는 앞서 설명한 것처럼 변경된 부분만 실행되는 구조, 의존성 순서 자동 관리, CI 결과 재사용 등의 장점을 활용하기 위함이다.
기존에도 concurrently 라이브러리를 사용해 빌드 명령어를 병렬 실행하고 있었다. 하지만 concurrently는 단순 병렬 실행만 지원하기 때문에, 모든 앱과 패키지의 로그가 한꺼번에 뒤섞여 출력되는 문제가 있었다.
이 문제를 해결하는 동시에, Turborepo의 장점을 활용해 전체적인 빌드 시간을 단축하는 것을 목표로 마이그레이션을 진행했다.
Turborepo에서 제공하는 Next.js 기반 모노레포 템플릿의 기본 구성은 다음과 같다.
| 도구 | 템플릿 버전 | 기존 프로젝트 버전 |
|---|---|---|
| Next.js | 15 | 14 |
| Tailwind CSS | v4 | v3 |
| ESLint | 9 | 8 |
Next.js는 버전 업그레이드 후 예상치 못한 이슈가 발생할 가능성을 고려해 14를 유지하기로 했다. Tailwind CSS와 ESLint는 메이저 버전 업데이트를 진행해도 문제없을 것이라 판단했다.
그런데 문제가 연달아 발생했다.
먼저 ESLint 쪽에서 문제가 생겼다. eslint-config-next의 버전을 14로 낮추자 ESLint 9와 호환되지 않는 문제가 발생했다. 이미 ESLint 9 기준의 Flat Config 설정까지 모두 적용해둔 상태였기 때문에, 결국 ESLint를 8로 다시 낮추고 설정을 처음부터 다시 구성해야 했다.
설정을 모두 마쳤음에도 린트가 정상 동작하지 않는 문제가 또 발생했다. 원인은 두 가지였다.
eslint-plugin-*)이나 설정(eslint-config-*)을 호출된 위치를 기준으로 node_modules를 탐색한다.결국 ESLint에서 "Module not found" 에러가 발생했고, 루트의 .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 설정이 많았기 때문에, v4로 완전히 마이그레이션하지 않고 @config 지시어를 활용해 기존 설정을 유지하는 방향으로 적용했다.
그런데 적용 후 확인해보니 Next.js 14에서는 Tailwind v4가 호환되지 않는다는 사실을 뒤늦게 알게 됐다. 결국 안정성을 우선으로 판단해 Tailwind CSS도 v3로 다운그레이드했다.
이 문제는 버전 호환성에 비하면 비교적 간단한 편이었다.
기존에는 Admin, Client 애플리케이션이 사용하는 공통 코드를 shared라는 하나의 패키지로만 관리하고 있었다. 하지만 변경된 부분만 빌드·테스트하는 Turborepo의 장점을 최대한 활용하기 위해, 역할에 따라 패키지를 분리했다.
utilstypesTurborepo는 패키지 간 의존 관계를 기반으로 그래프를 생성해 작업 순서를 결정한다.
A → B → C 의존성이 있다면, 실행 순서는 C → B → A
그런데 패키지를 분리한 후 빌드를 실행해보니, A가 B를 참조하고 B가 다시 A를 참조하는 순환 의존성(Circular Dependency) 문제가 발생했다.
다행히 문제되는 코드를 적절한 위치로 이동시키는 방식으로 쉽게 해결할 수 있었다. 다만 사전에 고려하지 못했던 부분이라 다소 당황스러웠던 이슈였다.
가장 큰 성과는 역시 빌드 시간 단축이다.
Galaxy Book2 Pro 기준으로 로컬 빌드 시간을 측정한 결과는 다음과 같다.
| 구분 | 단축률 |
|---|---|
| Cold 빌드 | 약 17.8% 단축 |
| Warm 빌드 | 약 96.6% 단축 |
그 외에도 개발 경험이 전반적으로 개선됐다.
개발 서버 실행 간소화: 기존에는 pnpm client dev, pnpm admin dev처럼 각각 따로 실행해야 했지만, 이제는 dev 명령어 한 번으로 client와 admin을 동시에 실행할 수 있다.
로그 가독성 향상: GitHub Actions에서 CI 실패 시 기존에는 모든 로그가 뒤섞여 확인이 불편했는데, Turborepo 도입 후에는 패키지별 로그를 드롭다운으로 펼치고 닫을 수 있어 훨씬 수월해졌다.
이번 글은 기술적인 설명보다는, 이런 작업을 했고 그 과정에서 어떤 일이 있었는지를 공유하고 싶어서 작성하게 되었다. 이렇게 변경사항이 큰 마이그레이션은 처음이어서 좀 힘들긴 했지만, 그래도 꽤 재밌었던 것 같다.