이번에 회사에서 새 프로젝트를 시작하게 되었습니다. 하나의 시스템을 위해 3개의 서비스가 필요했는데, 이는 각각 웹+앱, 앱 전용, 웹 전용으로 구성됩니다. 기술 스택으로는 React와 React Native를 사용하기로 했습니다. 이 3가지 서비스에서 공통적으로 사용하는 모듈과 컴포넌트를 효율적으로 관리하기 위해 모노레포 방식을 도입하고, 이를 지원하는 도구로 Turborepo를 선택했습니다.
Turborepo는 JavaScript와 TypeScript 프로젝트에 최적화된 빌드 시스템입니다. 코드베이스 작업(린트, 빌드, 테스트 등)은 시간이 많이 소요되는데, Turborepo는 캐싱을 통해 이러한 작업을 빠르게 처리하고, CI 속도를 향상시킵니다. 또한 Vercel에서 개발한 도구라는 점에서 많은 장점이 있습니다.
기능
Turborepo는 고급 빌드 시스템 기술을 활용해 개발 속도를 크게 향상시킵니다. 캐싱을 사용하여 동일한 작업을 반복하지 않으며, 멀티태스킹 능력을 극대화하고, 스케줄링과 CPU 유휴 시간을 최소화합니다.
Turborepo는 여러 방법으로 설치할 수 있습니다. 아래 명령어로 전역 또는 로컬 설치가 가능합니다:
# 전역 설치
npm install turbo --global
yarn global add turbo
pnpm install turbo --global
로컬 설치는 아래 명령어를 사용합니다:
bash
복사편집
# 로컬 설치
npm install turbo --dev
yarn add turbo --dev --ignore-workspace-root-check
pnpm add turbo --save-dev --ignore-workspace-root-check
새로운 모노레포를 시작하려면 create-turbo 패키지를 사용합니다:
npx create-turbo@latest
yarn dlx create-turbo@latest
pnpm dlx create-turbo@latest
모노레포를 생성하면 다음과 같은 디렉터리 구조가 생성됩니다:
apps
- apps/docs
- apps/web
packages
- packages/eslint-config-custom
- packages/tsconfig
- packages/ui
각각의 폴더는 package.json을 포함하며 독립적으로 코드를 작성하고 의존성을 관리할 수 있습니다. 다만, 다른 작업 공간에서 코드를 재사용할 수 있습니다.
예를 들어, apps/web은 packages/ui 패키지를 의존하고 있습니다. 즉, web 앱이 ui라는 로컬 패키지를 사용하는 구조입니다. 이는 다른 작업 공간 간 코드 공유를 용이하게 만듭니다.
// apps/web/package.json
{
"name": "web",
"dependencies": {
"ui": "workspace:*"
}
}
모노레포 내에서 애플리케이션을 넘어 코드와 설정을 공유하는 패턴은 매우 유용합니다.
각각의 앱은 공통으로 사용하는 컴포넌트나 모듈을 import
하여 사용할 수 있습니다. 예를 들어, ui 패키지에서 Button
컴포넌트를 정의한 후, 이를 web과 docs 애플리케이션에서 불러와 사용합니다.
// packages/ui/index.tsx
export * from "./Button";
이렇게 작성된 코드는 apps/web
과 apps/docs
에서 모두 사용할 수 있습니다.
모노레포의 모든 패키지에서 tsconfig를 공유하는 설정이 필요할 때, packages/tsconfig
폴더에 설정을 둡니다. 예를 들어:
// packages/tsconfig/package.json
{
"name": "tsconfig",
"version": "0.0.0",
"private": true,
"license": "MIT",
"files": ["base.json", "nextjs.json", "react-library.json"]
}
그리고 각 애플리케이션은 이 설정을 extends
로 참조합니다.
// packages/ui/tsconfig.json
{
"extends": "tsconfig/react-library.json"
}
이렇게 공통 설정을 활용하면 중복을 줄이고, 코드 관리가 쉬워집니다.
eslint-config-custom은 eslint 설정을 모노레포 내 모든 작업 공간에서 공유할 수 있게 해줍니다. 예를 들어, packages/eslint-config-custom 폴더에 ESLint 설정 파일을 두고, 각 작업 공간에서 이를 참조합니다.
// packages/eslint-config-custom/.eslintrc.js
module.exports = {
extends: ["next", "turbo", "prettier"]
};
이를 통해, docs, web, ui 등 모든 애플리케이션이 동일한 ESLint 규칙을 따를 수 있습니다.
Turborepo의 핵심 설정 파일은 turbo.json입니다. 여기서 각 파이프라인의 작업 흐름을 정의할 수 있습니다. 예를 들어, build 파이프라인에서는 출력 디렉터리를 지정할 수 있습니다.
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true}
}
}
Turborepo는 여러 작업 공간에서 lint를 동시에 실행할 수 있습니다. 예를 들어, turbo lint
명령어를 실행하면 모든 작업 공간에서 정의된 lint 스크립트를 동시에 실행합니다.
turbo lint
이 명령어를 실행하면, docs:lint, web:lint, ui:lint 등의 스크립트가 동시에 실행됩니다.
Turborepo는 이전 실행의 결과를 캐시하여, 동일한 작업을 반복하지 않습니다. 두 번째로 turbo lint
를 실행하면, 캐시된 결과를 사용하여 실행 시간을 단축시킵니다.
빌드 명령어를 실행할 때도, 이전 빌드 결과를 캐시하여 빠르게 빌드를 완료합니다. 예를 들어, turbo build
를 실행하면, 캐시된 결과를 복원하고, 필요한 경우에만 다시 빌드를 실행합니다.
turbo build
turbo dev
는 각 애플리케이션의 개발 서버를 실행합니다. 기본적으로 모든 작업 공간에서 dev
스크립트를 실행하지만, 특정 작업 공간만 실행하고 싶다면 --filter
플래그를 사용할 수 있습니다.
turbo dev --filter docs
이 명령어는 docs 작업 공간에서만 dev 서버를 실행합니다.