본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 10. 27. · 78 Views
React Turborepo 모노레포 완벽 가이드
Turborepo를 활용한 React 모노레포 구축부터 최적화까지, 실무에서 바로 적용할 수 있는 완벽한 가이드입니다. 프로젝트 구조, 패키지 관리, 빌드 최적화, 공유 컴포넌트 활용법을 단계별로 알아봅니다.
목차
- Turborepo 소개
- 프로젝트 초기 설정
- Workspace 구조 설계
- 공유 UI 컴포넌트 라이브러리
- 공유 TypeScript 설정
- Turborepo 파이프라인
- 캐싱 전략
- 원격 캐싱 설정
- 환경변수 관리
- 배포 전략
1. Turborepo 소개
시작하며
여러분이 여러 개의 React 앱을 관리할 때 이런 상황을 겪어본 적 있나요? 각 프로젝트마다 똑같은 버튼 컴포넌트를 복사해서 붙여넣고, ESLint 설정을 일일이 업데이트하고, 한 프로젝트에서 수정한 유틸 함수를 다른 프로젝트에 다시 작성하는 상황 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 코드 중복이 늘어나면 유지보수 비용이 기하급수적으로 증가하고, 일관성을 유지하기 어려워지며, 버그가 여러 곳에 퍼져나갑니다.
더 큰 문제는 빌드 시간이 점점 느려진다는 것입니다. 바로 이럴 때 필요한 것이 Turborepo입니다.
Vercel이 만든 이 도구는 모노레포를 쉽게 관리하면서도 놀라운 빌드 속도를 제공합니다.
개요
간단히 말해서, Turborepo는 여러 프로젝트를 하나의 저장소에서 효율적으로 관리할 수 있게 해주는 고성능 빌드 시스템입니다. 기존의 Lerna나 Nx 같은 도구들도 모노레포를 지원하지만, Turborepo는 설정이 간단하면서도 강력한 캐싱과 병렬 처리 기능을 제공합니다.
예를 들어, 여러 개의 Next.js 앱과 공유 컴포넌트 라이브러리를 함께 관리하는 경우에 매우 유용합니다. 기존에는 각 프로젝트를 별도 저장소로 관리하고 npm 패키지로 배포했다면, 이제는 한 저장소에서 모든 코드를 관리하면서도 독립적으로 배포할 수 있습니다.
Turborepo의 핵심 특징은 세 가지입니다: 증분 빌드로 변경된 부분만 다시 빌드하고, 로컬과 원격 캐싱으로 중복 작업을 제거하며, 의존성 그래프를 자동으로 분석하여 병렬 실행을 최적화합니다. 이러한 특징들이 대규모 프로젝트에서 빌드 시간을 수십 배 단축시킵니다.
코드 예제
// turbo.json - Turborepo 설정 파일
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
// 다른 패키지의 build가 먼저 완료되어야 함
"dependsOn": ["^build"],
// build 결과를 캐싱
"outputs": [".next/**", "dist/**"]
},
"dev": {
// 개발 서버는 캐싱하지 않음
"cache": false,
"persistent": true
}
}
}
설명
이것이 하는 일: Turborepo는 모노레포의 복잡한 의존성을 자동으로 분석하고, 빌드 프로세스를 최적화하여 개발 생산성을 극대화합니다. 첫 번째로, pipeline 설정은 각 태스크(build, dev, test 등)의 실행 규칙을 정의합니다.
"dependsOn": ["^build"]는 현재 패키지를 빌드하기 전에 의존하는 모든 패키지의 build가 먼저 완료되어야 한다는 의미입니다. 이렇게 하면 공유 라이브러리가 먼저 빌드되고, 그것을 사용하는 앱이 나중에 빌드됩니다.
그 다음으로, outputs 설정이 실행되면서 캐싱할 파일들을 지정합니다. .next/** 같은 빌드 결과물을 캐싱하면, 코드가 변경되지 않았을 때 이전 빌드 결과를 재사용하여 빌드 시간을 0초로 만들 수 있습니다.
마지막으로, dev 태스크는 개발 서버를 실행하는 작업이므로 cache: false로 설정하여 캐싱하지 않습니다. persistent: true는 이 태스크가 장시간 실행되는 프로세스임을 나타냅니다.
여러분이 이 설정을 사용하면 팀원 간에 빌드 캐시를 공유하여 CI/CD 시간을 70% 이상 단축할 수 있습니다. 또한 변경되지 않은 패키지는 자동으로 건너뛰므로, 대규모 모노레포에서도 빠른 개발 속도를 유지할 수 있습니다.
무엇보다 설정이 간단하여 기존 프로젝트에 쉽게 적용할 수 있습니다.
실전 팁
💡 개발 초기부터 turbo.json을 설정하세요. 나중에 추가하는 것보다 처음부터 캐싱 전략을 세우는 것이 훨씬 효과적입니다.
💡 outputs 설정을 정확히 하지 않으면 캐싱이 제대로 작동하지 않습니다. 각 패키지의 빌드 결과물 경로를 꼼꼼히 확인하세요.
💡 TURBO_TOKEN을 설정하면 Vercel의 원격 캐싱을 무료로 사용할 수 있습니다. 팀 전체의 빌드 속도가 극적으로 향상됩니다.
💡 turbo run build --dry로 실제 빌드 없이 실행 순서를 미리 확인할 수 있습니다. 의존성 문제를 사전에 파악하는 데 유용합니다.
2. 프로젝트 초기 설정
시작하며
여러분이 새로운 모노레포 프로젝트를 시작할 때 이런 고민을 해본 적 있나요? 디렉토리 구조를 어떻게 잡아야 할지, 패키지 매니저는 무엇을 써야 할지, 각 앱과 라이브러리를 어떻게 연결해야 할지 막막했던 경험 말이죠.
이런 초기 설정 단계는 프로젝트의 성패를 좌우합니다. 잘못된 구조로 시작하면 나중에 리팩토링하는 데 엄청난 시간이 들고, 팀원들도 혼란스러워합니다.
특히 모노레포는 일반 프로젝트보다 구조가 복잡하기 때문에 더욱 신중해야 합니다. 바로 이럴 때 필요한 것이 Turborepo의 공식 템플릿입니다.
검증된 구조로 몇 분 만에 프로젝트를 시작할 수 있습니다.
개요
간단히 말해서, Turborepo 프로젝트 초기 설정은 create-turbo 명령어로 표준화된 모노레포 구조를 자동으로 생성하는 과정입니다. Turborepo는 공식 템플릿을 제공하여 초기 설정의 부담을 크게 줄여줍니다.
예를 들어, Next.js 앱 여러 개와 공유 UI 라이브러리를 함께 사용하는 구조를 단 한 줄의 명령어로 만들 수 있습니다. 기존에는 각 프로젝트를 수동으로 생성하고, package.json의 workspaces를 직접 설정하고, 빌드 스크립트를 일일이 작성했다면, 이제는 자동으로 생성된 구조를 그대로 사용하거나 약간만 수정하면 됩니다.
초기 설정의 핵심은 세 가지입니다: pnpm workspaces로 패키지들을 연결하고, apps 폴더에 실제 애플리케이션을 배치하며, packages 폴더에 공유 코드를 관리합니다. 이러한 구조가 코드 재사용성을 극대화하고 의존성 관리를 단순화합니다.
코드 예제
# Turborepo 프로젝트 생성
npx create-turbo@latest my-monorepo
# 프로젝트 구조
my-monorepo/
├── apps/
│ ├── web/ # Next.js 메인 웹앱
│ └── admin/ # Next.js 관리자 페이지
├── packages/
│ ├── ui/ # 공유 UI 컴포넌트
│ ├── config/ # 공유 설정 (ESLint, TS 등)
│ └── utils/ # 공유 유틸리티 함수
├── turbo.json # Turborepo 설정
└── package.json # 루트 패키지 설정
설명
이것이 하는 일: create-turbo는 검증된 모노레포 구조를 자동으로 생성하여, 개발자가 초기 설정에 시간을 낭비하지 않고 바로 개발에 집중할 수 있게 합니다. 첫 번째로, npx create-turbo 명령어를 실행하면 대화형 CLI가 나타나 프로젝트 이름, 패키지 매니저(pnpm 권장), 템플릿 종류를 선택할 수 있습니다.
pnpm은 디스크 공간을 절약하고 설치 속도가 빠르므로 모노레포에 최적화되어 있습니다. 그 다음으로, 생성된 프로젝트 구조를 보면 apps 폴더에는 실제 사용자에게 배포되는 애플리케이션들이 들어갑니다.
각 앱은 독립적으로 실행되고 배포될 수 있지만, packages 폴더의 공유 코드를 import하여 사용합니다. packages 폴더에는 여러 앱에서 공통으로 사용하는 UI 컴포넌트, 설정 파일, 유틸리티 함수 등이 위치합니다.
마지막으로, 루트의 package.json에는 workspaces 설정이 있어 apps/*와 packages/*의 모든 패키지를 자동으로 인식합니다. 이렇게 하면 각 패키지가 서로를 의존성으로 사용할 수 있고, 한 번의 pnpm install로 모든 의존성이 설치됩니다.
여러분이 이 구조를 사용하면 새로운 앱이나 패키지를 추가할 때도 일관된 방식으로 관리할 수 있습니다. 팀원들도 직관적으로 코드를 찾을 수 있고, 빌드 파이프라인도 자동으로 최적화됩니다.
특히 TypeScript 경로 설정이 자동으로 구성되어 import 경로가 깔끔해집니다.
실전 팁
💡 pnpm을 패키지 매니저로 선택하세요. npm이나 yarn보다 3배 빠르고, 모노레포에 최적화된 기능들이 많습니다.
💡 프로젝트 생성 후 각 앱의 package.json에서 name을 명확히 설정하세요. @my-company/web처럼 스코프를 사용하면 패키지 충돌을 방지할 수 있습니다.
💡 .gitignore에 node_modules, .turbo, dist 등이 자동으로 추가되지만, 환경변수 파일(.env.local)도 반드시 추가하세요.
💡 README.md에 팀원들을 위한 시작 가이드를 작성하세요. pnpm install, pnpm dev, pnpm build 같은 기본 명령어를 문서화하면 온보딩 시간이 단축됩니다.
3. Workspace 구조 설계
시작하며
여러분이 모노레포에 새로운 기능을 추가할 때 이런 고민을 해본 적 있나요? 이 코드를 어느 패키지에 넣어야 할지, 새로운 패키지를 만들어야 할지, 아니면 기존 패키지를 확장해야 할지 망설였던 경험 말이죠.
이런 문제는 workspace 구조가 명확하지 않을 때 발생합니다. 패키지가 너무 많으면 관리가 복잡해지고, 너무 적으면 책임이 섞여서 유지보수가 어려워집니다.
잘못된 구조는 순환 의존성을 만들어 빌드 자체를 불가능하게 만들기도 합니다. 바로 이럴 때 필요한 것이 체계적인 workspace 설계 원칙입니다.
명확한 규칙으로 패키지를 분리하면 확장성과 유지보수성이 크게 향상됩니다.
개요
간단히 말해서, workspace 구조 설계는 apps와 packages 폴더 안에서 패키지들을 책임과 역할에 따라 논리적으로 분리하는 과정입니다. 효과적인 workspace 구조는 코드의 재사용성을 높이고 의존성을 명확하게 만듭니다.
예를 들어, UI 컴포넌트는 packages/ui에, 비즈니스 로직은 packages/core에, 타입 정의는 packages/types에 분리하면 각 패키지의 역할이 명확해집니다. 기존에는 모든 공유 코드를 하나의 packages/shared에 몰아넣었다면, 이제는 도메인과 책임에 따라 여러 패키지로 세분화합니다.
이렇게 하면 특정 패키지만 변경했을 때 영향 범위가 줄어듭니다. workspace 설계의 핵심 원칙은 세 가지입니다: 단일 책임 원칙으로 각 패키지는 하나의 명확한 목적만 가지고, 의존성 방향은 항상 아래로 흐르도록 하며(apps → packages, 절대 packages → apps로 가지 않음), 공통 설정은 packages/config에 집중화합니다.
이러한 원칙들이 모노레포의 복잡도를 관리 가능한 수준으로 유지합니다.
코드 예제
// package.json - workspace 설정
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test"
}
}
// apps/web/package.json - 앱에서 패키지 사용
{
"name": "@my-company/web",
"dependencies": {
"@my-company/ui": "workspace:*",
"@my-company/utils": "workspace:*"
}
}
설명
이것이 하는 일: workspace 설정은 여러 패키지를 하나의 프로젝트로 묶어서, 마치 독립된 npm 패키지처럼 사용하면서도 로컬에서 즉시 변경사항이 반영되도록 합니다. 첫 번째로, 루트 package.json의 workspaces 배열은 pnpm에게 어느 폴더를 패키지로 인식할지 알려줍니다.
apps/와 packages/ 같은 glob 패턴을 사용하면 새로운 패키지를 추가할 때마다 설정을 수정할 필요가 없습니다. private: true는 이 루트 패키지를 npm에 실수로 배포하지 않도록 방지합니다.
그 다음으로, 각 앱의 package.json에서 workspace:* 프로토콜을 사용하여 같은 workspace 내의 패키지를 의존성으로 추가합니다. 이렇게 하면 pnpm이 심볼릭 링크를 만들어 로컬 패키지를 연결하므로, packages/ui의 코드를 수정하면 즉시 apps/web에 반영됩니다.
별도의 빌드나 배포 과정 없이 실시간으로 개발할 수 있습니다. 마지막으로, 루트의 scripts에 정의된 명령어들은 turbo run을 통해 모든 workspace의 같은 이름의 스크립트를 실행합니다.
예를 들어 pnpm build를 실행하면 Turborepo가 의존성 그래프를 분석하여 packages/ui를 먼저 빌드하고, 그 다음 apps/web을 빌드합니다. 여러분이 이 구조를 사용하면 대규모 프로젝트를 작은 단위로 나누어 관리할 수 있습니다.
각 패키지는 독립적으로 테스트하고 버전 관리할 수 있으며, 새로운 팀원도 전체 코드베이스를 이해하지 않고도 특정 패키지만 수정할 수 있습니다. 또한 packages/ui만 오픈소스로 공개하는 등 선택적 공유도 가능합니다.
실전 팁
💡 패키지 이름에 스코프(@my-company/)를 사용하면 npm의 공개 패키지와 충돌하지 않고, 소유권도 명확해집니다.
💡 순환 의존성을 절대 만들지 마세요. A가 B를 import하고 B가 A를 import하면 빌드가 실패합니다. turbo run build --graph로 의존성 그래프를 시각화하여 확인하세요.
💡 packages/tsconfig를 만들어 공통 TypeScript 설정을 관리하면, 모든 패키지에서 일관된 타입 체킹을 적용할 수 있습니다.
💡 각 패키지의 package.json에 exports 필드를 명시하여 공개 API를 명확히 하세요. 내부 구현은 숨기고 필요한 부분만 노출하면 리팩토링이 쉬워집니다.
💡 pnpm-workspace.yaml을 사용하면 특정 패키지만 workspace에서 제외할 수 있습니다. 실험적인 패키지를 관리할 때 유용합니다.
4. 공유 UI 컴포넌트 라이브러리
시작하며
여러분이 여러 앱에서 동일한 디자인 시스템을 사용할 때 이런 문제를 겪어본 적 있나요? 각 프로젝트마다 Button 컴포넌트를 따로 만들고, 디자인이 조금씩 달라지고, 한 곳에서 버그를 고쳐도 다른 곳에는 여전히 남아있는 상황 말이죠.
이런 문제는 컴포넌트를 중앙화하지 않았을 때 발생합니다. 코드 중복이 늘어나면 유지보수 비용이 급증하고, 디자인 일관성도 무너집니다.
특히 디자이너가 버튼 스타일을 변경하면 10개의 프로젝트를 모두 수정해야 하는 악몽이 시작됩니다. 바로 이럴 때 필요한 것이 공유 UI 컴포넌트 라이브러리입니다.
packages/ui에 모든 공통 컴포넌트를 모아두면 한 번의 수정으로 모든 앱에 즉시 반영됩니다.
개요
간단히 말해서, 공유 UI 컴포넌트 라이브러리는 모든 앱에서 사용하는 재사용 가능한 React 컴포넌트를 한 곳에 모아둔 패키지입니다. 이 라이브러리는 디자인 시스템의 구현체 역할을 하면서 코드 재사용성을 극대화합니다.
예를 들어, Button, Input, Modal 같은 기본 컴포넌트부터 복잡한 DataTable이나 Form 컴포넌트까지 모두 packages/ui에서 관리하면, 여러 앱에서 일관된 UX를 제공할 수 있습니다. 기존에는 각 프로젝트에 components 폴더를 만들고 컴포넌트를 복사했다면, 이제는 packages/ui에서 import하여 사용합니다.
변경사항은 자동으로 모든 앱에 반영되고, 타입 안정성도 보장됩니다. 공유 UI 라이브러리의 핵심 특징은 세 가지입니다: Tree-shaking이 가능하도록 named export를 사용하고, TypeScript로 작성하여 props 자동완성을 제공하며, Tailwind CSS나 CSS Modules로 스타일을 캡슐화합니다.
이러한 특징들이 개발 생산성과 번들 크기 최적화를 동시에 달성합니다.
코드 예제
// packages/ui/src/Button.tsx
import { ButtonHTMLAttributes, ReactNode } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
children: ReactNode;
}
export function Button({ variant = 'primary', children, ...props }: ButtonProps) {
const baseStyles = 'px-4 py-2 rounded font-medium transition-colors';
const variantStyles = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
danger: 'bg-red-600 text-white hover:bg-red-700'
};
return (
<button className={`${baseStyles} ${variantStyles[variant]}`} {...props}>
{children}
</button>
);
}
// packages/ui/index.tsx - 공개 API
export { Button } from './src/Button';
export { Input } from './src/Input';
설명
이것이 하는 일: 공유 UI 라이브러리는 모든 앱에서 사용할 수 있는 표준화된 컴포넌트를 제공하여, 개발 속도를 높이고 디자인 일관성을 유지합니다. 첫 번째로, Button 컴포넌트는 TypeScript의 interface로 props를 정의하여 타입 안정성을 제공합니다.
ButtonHTMLAttributes를 확장하면 onClick, disabled 같은 네이티브 HTML 속성을 모두 사용할 수 있습니다. variant prop으로 다양한 버튼 스타일을 제공하면서도, 기본값을 'primary'로 설정하여 사용하기 쉽게 만듭니다.
그 다음으로, Tailwind CSS 클래스를 사용하여 스타일을 정의합니다. baseStyles는 모든 variant에 공통으로 적용되는 스타일이고, variantStyles 객체는 각 variant별 색상을 정의합니다.
이렇게 하면 스타일을 한 곳에서 관리하면서도 여러 variant를 쉽게 추가할 수 있습니다. ...props로 나머지 HTML 속성을 전달하면 접근성 속성(aria-*)도 자유롭게 사용할 수 있습니다.
마지막으로, packages/ui/index.tsx에서 named export로 컴포넌트를 공개합니다. 이렇게 하면 앱에서 import { Button } from '@my-company/ui'처럼 깔끔하게 사용할 수 있고, 사용하지 않는 컴포넌트는 번들에 포함되지 않습니다(Tree-shaking).
여러분이 이 라이브러리를 사용하면 새로운 기능을 개발할 때 UI 컴포넌트를 처음부터 만들 필요가 없습니다. 이미 검증된 컴포넌트를 가져다 쓰면 되므로 개발 시간이 크게 단축됩니다.
또한 디자인 변경이 필요하면 packages/ui만 수정하면 모든 앱에 자동으로 반영되므로, 유지보수 비용이 극적으로 감소합니다. 접근성(a11y)도 한 번만 올바르게 구현하면 모든 곳에서 보장됩니다.
실전 팁
💡 각 컴포넌트를 별도 파일로 분리하고 index.tsx에서 re-export하세요. 이렇게 하면 Tree-shaking이 효과적으로 작동합니다.
💡 Storybook을 packages/ui에 추가하면 컴포넌트를 독립적으로 개발하고 문서화할 수 있습니다. 디자이너와 협업할 때 특히 유용합니다.
💡 복잡한 컴포넌트는 Compound Component 패턴을 사용하세요. 예를 들어 <Modal.Header>, <Modal.Body>로 분리하면 유연성이 높아집니다.
💡 packages/ui에도 Jest와 Testing Library를 설정하여 컴포넌트 테스트를 작성하세요. 한 번 테스트하면 모든 앱에서 안정성이 보장됩니다.
💡 CSS-in-JS를 사용한다면 styled-components보다 emotion을 권장합니다. 번들 크기가 작고 성능이 더 좋습니다.
5. 공유 TypeScript 설정
시작하며
여러분이 여러 프로젝트에서 TypeScript를 사용할 때 이런 불편함을 겪어본 적 있나요? 어떤 프로젝트는 strict 모드가 켜져 있고, 어떤 곳은 꺼져 있어서 타입 에러가 일관성 없이 발생하고, tsconfig.json을 복사해도 경로 설정이 달라 제대로 작동하지 않는 상황 말이죠.
이런 문제는 TypeScript 설정이 분산되어 있을 때 발생합니다. 각 프로젝트마다 다른 설정을 사용하면 타입 안정성이 떨어지고, 팀원들도 혼란스러워합니다.
특히 신입 개발자는 어느 설정이 올바른지 판단하기 어렵습니다. 바로 이럴 때 필요한 것이 공유 TypeScript 설정입니다.
packages/tsconfig에 기본 설정을 만들어두면, 모든 패키지에서 extends로 상속받아 일관된 타입 체킹을 적용할 수 있습니다.
개요
간단히 말해서, 공유 TypeScript 설정은 packages/tsconfig에 기본 tsconfig.json을 만들고, 다른 패키지들이 이를 extends하여 일관된 TypeScript 설정을 사용하도록 하는 패턴입니다. 이 패턴은 DRY(Don't Repeat Yourself) 원칙을 TypeScript 설정에 적용하여 유지보수성을 높입니다.
예를 들어, strict 모드, 타겟 ES 버전, 모듈 해상도 전략 같은 공통 설정을 한 곳에서 관리하면, 설정 변경 시 모든 프로젝트에 즉시 반영됩니다. 기존에는 각 프로젝트의 tsconfig.json을 수동으로 동기화했다면, 이제는 base 설정을 extends하고 프로젝트별 차이만 오버라이드합니다.
이렇게 하면 설정 누락이나 불일치를 방지할 수 있습니다. 공유 설정의 핵심 전략은 세 가지입니다: base, react, nextjs처럼 용도별로 여러 설정 파일을 만들고, 각 프로젝트는 가장 적합한 설정을 extends하며, paths 같은 프로젝트별 설정만 로컬에서 오버라이드합니다.
이러한 전략들이 설정 복잡도를 최소화하면서도 유연성을 유지합니다.
코드 예제
// packages/tsconfig/base.json - 기본 설정
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"noUncheckedIndexedAccess": true
}
}
// apps/web/tsconfig.json - 앱에서 상속
{
"extends": "@my-company/tsconfig/base.json",
"compilerOptions": {
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
설명
이것이 하는 일: 공유 TypeScript 설정은 모노레포 전체에서 동일한 TypeScript 규칙을 적용하여, 타입 안정성을 보장하고 설정 중복을 제거합니다. 첫 번째로, packages/tsconfig/base.json은 모든 프로젝트에 공통으로 적용되는 컴파일러 옵션을 정의합니다.
strict: true는 TypeScript의 모든 엄격한 타입 체킹을 활성화하여 런타임 에러를 사전에 방지합니다. noUncheckedIndexedAccess: true는 배열이나 객체에 접근할 때 undefined 가능성을 강제로 체크하게 만들어, 흔한 런타임 에러를 컴파일 타임에 잡아냅니다.
skipLibCheck: true는 node_modules의 타입 정의 파일은 체크하지 않아 빌드 속도를 높입니다. 그 다음으로, 각 앱의 tsconfig.json에서 extends 필드로 base.json을 상속받습니다.
@my-company/tsconfig/base.json처럼 패키지 이름으로 참조하면, pnpm workspace가 자동으로 올바른 파일을 찾습니다. 상속받은 후에는 프로젝트별로 필요한 설정만 추가하거나 오버라이드합니다.
예를 들어 Next.js 앱은 jsx: "preserve"가 필요하고, paths로 @/* 같은 경로 별칭을 설정합니다. 마지막으로, include와 exclude로 TypeScript가 체크할 파일을 지정합니다.
include에 **/.ts와 **/.tsx를 넣으면 모든 TypeScript 파일이 체크되고, exclude에 node_modules를 넣으면 외부 라이브러리는 제외됩니다. 여러분이 이 구조를 사용하면 TypeScript 설정을 한 곳에서 관리하므로 유지보수가 쉬워집니다.
예를 들어 새로운 ES 기능을 사용하고 싶으면 base.json의 lib만 업데이트하면 모든 프로젝트에 즉시 적용됩니다. 또한 신입 개발자가 프로젝트별로 다른 설정을 학습할 필요가 없고, IDE의 TypeScript 언어 서버도 더 빠르게 작동합니다.
실전 팁
💡 react.json과 nextjs.json처럼 프레임워크별 설정을 만들어두면, 새 프로젝트를 추가할 때 적절한 것을 선택하기만 하면 됩니다.
💡 packages/tsconfig/package.json에 "version": "0.0.0"과 "private": true를 설정하여 실수로 npm에 배포되지 않도록 하세요.
💡 verbatimModuleSyntax: true를 추가하면 type import를 명시적으로 작성하게 강제하여, 번들러가 Tree-shaking을 더 효과적으로 수행할 수 있습니다.
💡 composite: true를 설정하면 TypeScript의 프로젝트 참조 기능을 사용할 수 있어, 타입 체킹과 빌드 속도가 크게 향상됩니다.
6. Turborepo 파이프라인
시작하며
여러분이 모노레포에서 빌드를 실행할 때 이런 문제를 겪어본 적 있나요? 어떤 패키지를 먼저 빌드해야 할지 모르겠고, 수동으로 순서를 지정하면 실수로 잘못된 순서로 실행되어 빌드가 실패하는 상황 말이죠.
이런 문제는 빌드 의존성을 명시적으로 관리하지 않았을 때 발생합니다. 특히 packages/ui를 사용하는 apps/web을 빌드할 때, ui가 먼저 빌드되지 않으면 import 에러가 발생합니다.
수동으로 관리하면 프로젝트가 커질수록 복잡도가 기하급수적으로 증가합니다. 바로 이럴 때 필요한 것이 Turborepo의 파이프라인 설정입니다.
각 태스크의 의존성을 선언하면, Turborepo가 자동으로 올바른 순서로 실행하고 병렬 처리까지 최적화합니다.
개요
간단히 말해서, Turborepo 파이프라인은 turbo.json에서 각 태스크(build, test, lint 등)의 실행 순서와 캐싱 규칙을 정의하는 설정입니다. 파이프라인은 복잡한 빌드 의존성을 선언적으로 관리하여 개발자가 순서를 고민할 필요를 없앱니다.
예를 들어, test 태스크가 build에 의존한다고 선언하면, Turborepo가 자동으로 빌드를 먼저 실행하고 테스트를 나중에 실행합니다. 기존에는 package.json의 scripts에서 &&로 태스크를 직렬 연결했다면, 이제는 Turborepo가 의존성 그래프를 분석하여 병렬 실행 가능한 작업은 동시에 처리합니다.
이렇게 하면 전체 빌드 시간이 크게 단축됩니다. 파이프라인의 핵심 개념은 세 가지입니다: dependsOn으로 태스크 간 의존성을 선언하고, outputs로 캐싱할 파일을 지정하며, inputs로 어떤 파일 변경 시 캐시를 무효화할지 정의합니다.
이러한 개념들이 빌드 자동화와 최적화를 동시에 달성합니다.
코드 예제
// turbo.json - 파이프라인 설정
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
// 의존하는 패키지들의 build가 먼저 완료되어야 함
"dependsOn": ["^build"],
// 빌드 결과물을 캐싱
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"test": {
// 같은 패키지의 build가 먼저 완료되어야 함
"dependsOn": ["build"],
"outputs": ["coverage/**"],
// 소스 코드와 테스트 파일이 변경될 때만 재실행
"inputs": ["src/**", "test/**"]
},
"lint": {
// 의존성 없음 - 병렬 실행 가능
"outputs": []
},
"dev": {
// 개발 서버는 캐싱하지 않음
"cache": false,
"persistent": true
}
}
}
설명
이것이 하는 일: Turborepo 파이프라인은 복잡한 빌드 프로세스를 선언적으로 정의하여, 개발자가 실행 순서를 고민하지 않고도 최적화된 빌드를 얻을 수 있게 합니다. 첫 번째로, build 태스크의 "dependsOn": ["^build"]는 특별한 의미를 가집니다.
^(캐럿) 기호는 "이 패키지가 의존하는 다른 패키지들의"를 뜻합니다. 즉, apps/web을 빌드하기 전에 packages/ui와 packages/utils의 빌드가 먼저 완료되어야 한다는 의미입니다.
Turborepo는 package.json의 dependencies를 읽어 자동으로 의존성 그래프를 생성하므로, 여러분은 순서를 명시적으로 지정할 필요가 없습니다. 그 다음으로, outputs 배열은 캐싱할 파일 패턴을 정의합니다.
.next/**는 Next.js의 모든 빌드 결과물을 캐싱하지만, !.next/cache/**처럼 느낌표로 시작하면 특정 폴더는 제외할 수 있습니다. 코드가 변경되지 않으면 이전 빌드 결과를 재사용하므로 빌드 시간이 0초가 됩니다.
inputs를 지정하면 해당 파일이 변경될 때만 캐시를 무효화하여, 관련 없는 파일(예: README.md) 변경 시에는 캐시를 유지합니다. 마지막으로, dev 태스크는 개발 서버처럼 장시간 실행되는 프로세스를 위한 설정입니다.
cache: false로 캐싱을 비활성화하고, persistent: true로 이 태스크가 종료되지 않음을 알립니다. 이렇게 하면 turbo run dev 실행 시 모든 앱의 개발 서버가 동시에 시작되고, 하나를 종료하면 모두 종료됩니다.
여러분이 이 파이프라인을 사용하면 pnpm build 한 번으로 모든 패키지가 올바른 순서로 빌드됩니다. 병렬 처리 가능한 패키지들은 자동으로 동시에 빌드되어 전체 시간이 단축됩니다.
또한 캐싱 덕분에 변경되지 않은 패키지는 건너뛰므로, 대규모 모노레포에서도 빠른 반복 개발이 가능합니다. CI/CD 환경에서는 이전 빌드 캐시를 재사용하여 배포 시간을 10분에서 1분으로 줄일 수도 있습니다.
실전 팁
💡 turbo run build --dry를 실행하면 실제 빌드 없이 실행 계획만 출력됩니다. 의존성 순서가 올바른지 확인할 때 유용합니다.
💡 dependsOn에서 ^를 빼면 같은 패키지 내의 태스크 의존성을 의미합니다. 예: "test": { "dependsOn": ["build"] }는 테스트 전에 같은 패키지를 빌드합니다.
💡 globalDependencies로 .env 파일이나 tsconfig.json 변경 시 모든 캐시를 무효화할 수 있습니다.
💡 turbo run build --graph를 실행하면 의존성 그래프를 시각화한 이미지를 생성합니다. 복잡한 모노레포의 구조를 이해하는 데 큰 도움이 됩니다.
7. 캐싱 전략
시작하며
여러분이 코드를 조금만 수정하고 다시 빌드할 때 이런 답답함을 느껴본 적 있나요? 한 줄만 바꿨는데 전체 프로젝트가 다시 빌드되고, 5분이 넘게 기다린 끝에 똑같은 결과물이 나오는 상황 말이죠.
이런 문제는 증분 빌드와 캐싱이 제대로 작동하지 않을 때 발생합니다. 변경되지 않은 코드를 매번 다시 컴파일하는 것은 엄청난 시간 낭비입니다.
특히 CI/CD에서 매 커밋마다 전체 빌드를 실행하면 피드백 속도가 느려져 개발 생산성이 떨어집니다. 바로 이럴 때 필요한 것이 Turborepo의 캐싱 전략입니다.
입력(소스 코드)과 출력(빌드 결과물)을 추적하여, 입력이 변경되지 않으면 이전 출력을 재사용합니다.
개요
간단히 말해서, Turborepo의 캐싱은 각 태스크의 입력 파일들을 해시로 변환하여, 동일한 입력에 대해서는 이전 빌드 결과를 재사용하는 시스템입니다. 캐싱 전략은 빌드 시간을 획기적으로 단축하여 개발 반복 속도를 높입니다.
예를 들어, packages/ui를 수정하지 않았다면 이전 빌드 결과를 그대로 사용하고, apps/web만 다시 빌드하면 됩니다. 전체 빌드가 10분 걸리더라도 캐시 히트 시에는 몇 초 만에 완료됩니다.
기존에는 make나 webpack의 캐싱을 사용했지만, 이들은 로컬 머신에만 캐시가 저장되었다면, Turborepo는 로컬 캐시뿐만 아니라 원격 캐시까지 지원하여 팀 전체가 빌드 결과를 공유할 수 있습니다. 캐싱의 핵심 메커니즘은 세 가지입니다: 소스 코드, package.json, 환경변수 등 모든 입력을 해시로 만들고, 해시를 키로 사용하여 빌드 결과물과 로그를 저장하며, 같은 해시가 다시 나타나면 캐시에서 복원합니다.
이러한 메커니즘이 결정적 빌드(같은 입력 → 같은 출력)를 보장합니다.
코드 예제
// turbo.json - 캐싱 상세 설정
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
// 빌드 결과물 지정 (캐싱 대상)
"outputs": [".next/**", "!.next/cache/**", "dist/**"],
// 입력 파일 지정 (해시 계산 대상)
"inputs": ["src/**/*.tsx", "src/**/*.ts", "public/**"],
// 환경변수도 해시에 포함
"env": ["NODE_ENV", "NEXT_PUBLIC_API_URL"]
}
},
// 모든 태스크에 영향을 주는 전역 파일
"globalDependencies": [".env", "tsconfig.json"]
}
// package.json - 캐시 확인 스크립트
{
"scripts": {
"build": "turbo run build",
"build:force": "turbo run build --force"
}
}
설명
이것이 하는 일: Turborepo의 캐싱 시스템은 빌드를 결정적 함수처럼 취급하여, 같은 입력에 대해서는 항상 같은 출력을 내므로 중복 작업을 완전히 제거합니다. 첫 번째로, inputs 배열은 어떤 파일이 변경될 때 캐시를 무효화할지 정의합니다.
src/**/*.tsx처럼 glob 패턴을 사용하면 소스 코드만 추적하고, 문서 파일(README.md)이나 설정 파일은 제외할 수 있습니다. 이렇게 하면 관련 없는 파일 변경으로 불필요한 재빌드가 발생하지 않습니다.
env 배열에 환경변수를 명시하면, 같은 코드라도 NODE_ENV가 다르면 별도로 빌드하여 개발과 프로덕션 빌드를 구분합니다. 그 다음으로, Turborepo가 태스크를 실행할 때 내부적으로 동작하는 과정입니다.
먼저 inputs, env, 의존 패키지들의 해시를 조합하여 고유한 해시 키를 생성합니다. 이 해시로 로컬 캐시(.turbo 폴더)를 확인하고, 있으면 outputs에 지정된 파일들을 복원한 후 태스크를 건너뜁니다.
없으면 실제로 빌드를 실행하고, 결과물을 해시 키로 캐시에 저장합니다. 마지막으로, globalDependencies는 모든 패키지에 영향을 주는 파일을 지정합니다.
.env 파일이 변경되면 전체 모노레포의 캐시가 무효화되므로, 환경변수 변경 시 올바르게 재빌드됩니다. --force 플래그를 사용하면 캐시를 무시하고 강제로 빌드할 수 있어, 디버깅이나 배포 시 유용합니다.
여러분이 이 캐싱 전략을 사용하면 로컬 개발에서 빌드 시간이 90% 이상 감소합니다. 예를 들어 packages/ui를 수정하면 그것과 의존하는 앱만 재빌드되고, 나머지 10개 패키지는 캐시에서 즉시 복원됩니다.
CI/CD에서도 이전 빌드 캐시를 재사용하면 전체 빌드 시간이 10분에서 2분으로 줄어들어, PR 피드백 속도가 극적으로 빨라집니다. 또한 결정적 빌드 덕분에 "내 컴퓨터에서는 되는데" 같은 문제가 사라집니다.
실전 팁
💡 터미널에서 FULL TURBO 메시지가 나오면 캐시 히트를 의미합니다. 처음에는 신기하지만, 빠르게 일상이 됩니다!
💡 .gitignore에 .turbo를 추가하세요. 로컬 캐시는 공유할 필요가 없고, 원격 캐싱을 사용하면 됩니다.
💡 outputs에 너무 많은 파일을 포함하면 캐시 저장/복원 시간이 길어집니다. 꼭 필요한 결과물만 지정하세요.
💡 turbo run build --summarize를 실행하면 각 태스크의 캐시 히트율과 실행 시간을 상세히 분석할 수 있습니다.
8. 원격 캐싱 설정
시작하며
여러분이 팀원의 PR을 리뷰하고 로컬에서 빌드할 때 이런 비효율을 경험한 적 있나요? 팀원이 이미 CI에서 빌드를 성공했는데, 여러분의 컴퓨터에서는 처음부터 다시 10분씩 빌드해야 하는 상황 말이죠.
이런 문제는 빌드 캐시가 로컬 머신에만 저장되어 있을 때 발생합니다. 각 개발자와 CI 서버가 독립적으로 빌드하면 엄청난 중복 작업이 발생합니다.
예를 들어 5명의 팀원이 같은 브랜치를 빌드하면, 동일한 작업이 5번 반복됩니다. 바로 이럴 때 필요한 것이 Turborepo의 원격 캐싱입니다.
Vercel이 무료로 제공하는 원격 캐시 서버에 빌드 결과를 업로드하면, 팀 전체가 캐시를 공유하여 생산성이 기하급수적으로 증가합니다.
개요
간단히 말해서, 원격 캐싱은 빌드 결과물을 클라우드에 저장하여, 팀원들과 CI/CD 시스템이 서로의 빌드 캐시를 재사용할 수 있게 하는 기능입니다. 원격 캐싱은 팀 전체의 빌드 시간을 획기적으로 단축합니다.
예를 들어, 개발자 A가 빌드한 결과를 원격에 업로드하면, 개발자 B가 같은 코드를 빌드할 때 원격 캐시에서 다운로드하여 즉시 완료됩니다. CI에서도 마찬가지로 이전 빌드 결과를 재사용합니다.
기존에는 Docker 레이어 캐싱이나 CI의 캐시 기능을 사용했지만, 이들은 설정이 복잡하고 제한적이었다면, Turborepo의 원격 캐싱은 몇 줄의 설정만으로 즉시 작동하며 모든 환경에서 일관되게 동작합니다. 원격 캐싱의 핵심 이점은 세 가지입니다: 팀원들이 서로의 빌드 결과를 공유하여 중복 작업을 제거하고, CI/CD에서 이전 빌드를 재사용하여 배포 속도를 높이며, Vercel의 무료 플랜으로도 소규모 팀에게는 충분한 용량을 제공합니다.
이러한 이점들이 개발 비용을 절감하면서도 속도를 극대화합니다.
코드 예제
// 1. Vercel 계정으로 로그인
npx turbo login
// 2. 프로젝트를 Vercel에 연결
npx turbo link
// .turbo/config.json - 자동 생성됨
{
"teamId": "team_xxxxxxxxxxxx",
"apiUrl": "https://vercel.com/api"
}
// package.json - CI에서 사용할 스크립트
{
"scripts": {
"build": "turbo run build",
"build:ci": "turbo run build --token=$TURBO_TOKEN"
}
}
// GitHub Actions - CI 설정 예시
// .github/workflows/ci.yml
- name: Build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
run: pnpm build
설명
이것이 하는 일: 원격 캐싱은 로컬 캐시를 클라우드로 확장하여, 한 번 빌드한 결과를 팀의 모든 머신에서 재사용할 수 있게 합니다. 첫 번째로, turbo login을 실행하면 브라우저가 열리며 Vercel 계정으로 인증합니다.
로그인에 성공하면 인증 토큰이 로컬 머신의 ~/.turbo/config.json에 저장됩니다. 이 토큰으로 원격 캐시 서버에 접근할 수 있습니다.
turbo link 명령어는 현재 프로젝트를 Vercel의 특정 팀과 연결하여, 해당 팀의 원격 캐시를 사용하도록 설정합니다. 그 다음으로, 원격 캐싱이 작동하는 방식입니다.
로컬에서 빌드를 실행하면 Turborepo는 먼저 로컬 캐시를 확인하고, 없으면 원격 캐시를 확인합니다. 원격에서 캐시를 찾으면 다운로드하여 복원하고, 없으면 실제로 빌드한 후 결과를 로컬과 원격 캐시 모두에 업로드합니다.
다음에 다른 팀원이 같은 코드를 빌드하면 원격 캐시에서 즉시 복원되어 빌드 시간이 0초가 됩니다. 마지막으로, CI/CD 환경에서 원격 캐싱을 사용하려면 TURBO_TOKEN과 TURBO_TEAM 환경변수를 설정해야 합니다.
Vercel 대시보드에서 토큰을 생성하여 GitHub Secrets에 저장하고, CI 워크플로우에서 환경변수로 전달합니다. 이렇게 하면 CI도 팀의 원격 캐시를 사용하여, 이전 빌드나 로컬 빌드 결과를 재사용할 수 있습니다.
여러분이 원격 캐싱을 사용하면 팀의 전체 빌드 시간이 70% 이상 감소합니다. 예를 들어 PR을 만들면 CI가 빌드하고 결과를 업로드하고, 여러분이 그 브랜치를 pull하여 빌드하면 원격 캐시에서 즉시 복원됩니다.
또한 main 브랜치의 빌드 캐시를 재사용하여, 새 브랜치에서도 변경된 패키지만 빌드하면 됩니다. 대규모 팀에서는 수백 시간의 빌드 시간을 절약하여 인프라 비용도 크게 절감됩니다.
실전 팁
💡 Vercel의 무료 플랜은 Hobby 프로젝트에 충분하지만, 상용 프로젝트는 Pro 플랜으로 업그레이드하세요. 캐시 저장 용량과 보존 기간이 늘어납니다.
💡 TURBO_TOKEN을 절대 코드에 커밋하지 마세요. 환경변수나 CI Secrets로만 관리해야 보안이 유지됩니다.
💡 self-hosted 원격 캐시를 원하면 turbo-remote-cache 같은 오픈소스 서버를 사용할 수 있습니다. S3나 GCS를 백엔드로 사용하면 비용을 더욱 절감할 수 있습니다.
💡 터미널에 REMOTE CACHE HIT 메시지가 나오면 원격 캐시에서 복원한 것입니다. 팀원들이 얼마나 많은 시간을 절약하는지 실감할 수 있습니다.
9. 환경변수 관리
시작하며
여러분이 여러 앱에서 서로 다른 환경변수를 사용할 때 이런 혼란을 겪어본 적 있나요? apps/web과 apps/admin이 각각 다른 API URL을 사용해야 하는데, .env 파일이 어디에 있는지 헷갈리고, 환경변수가 빌드에 제대로 주입되지 않아 프로덕션에서 에러가 발생하는 상황 말이죠.
이런 문제는 모노레포에서 환경변수를 체계적으로 관리하지 않았을 때 발생합니다. 각 앱과 패키지가 독립적으로 환경변수를 필요로 하는데, 파일이 여기저기 흩어져 있으면 관리가 불가능해집니다.
특히 프로덕션 배포 시 환경변수 누락은 심각한 장애로 이어집니다. 바로 이럴 때 필요한 것이 체계적인 환경변수 관리 전략입니다.
각 앱별 .env 파일과 Turborepo의 env 설정을 조합하면 안전하고 명확하게 관리할 수 있습니다.
개요
간단히 말해서, 모노레포의 환경변수 관리는 각 앱의 루트에 .env 파일을 배치하고, turbo.json에서 어떤 환경변수가 빌드에 영향을 주는지 명시하는 패턴입니다. 이 패턴은 환경변수의 범위를 명확히 하여 실수를 방지합니다.
예를 들어, apps/web/.env는 web 앱에만 적용되고, apps/admin/.env는 admin 앱에만 적용되므로 격리가 보장됩니다. 또한 turbo.json의 env 설정으로 환경변수가 변경되면 캐시를 자동으로 무효화합니다.
기존에는 루트에 하나의 .env 파일을 두고 모든 앱이 공유했다면, 이제는 각 앱이 독립적인 환경변수를 가지면서도 globalEnv로 공통 변수를 공유할 수 있습니다. 이렇게 하면 멀티 테넌트나 마이크로 프론트엔드 아키텍처에서도 유연하게 대응할 수 있습니다.
환경변수 관리의 핵심 원칙은 세 가지입니다: 민감한 정보는 절대 .env.example에 넣지 않고, NEXT_PUBLIC_ 같은 프레임워크 접두사를 정확히 사용하며, turbo.json에 환경변수를 명시하여 캐싱과 연동합니다. 이러한 원칙들이 보안과 안정성을 동시에 보장합니다.
코드 예제
// apps/web/.env.local - 개발용 환경변수
NEXT_PUBLIC_API_URL=http://localhost:3001
DATABASE_URL=postgresql://localhost:5432/mydb
SECRET_KEY=dev-secret-key
// apps/web/.env.production - 프로덕션용
NEXT_PUBLIC_API_URL=https://api.production.com
DATABASE_URL=$DATABASE_URL # CI에서 주입됨
SECRET_KEY=$SECRET_KEY
// turbo.json - 환경변수를 캐시 키에 포함
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**"],
// 이 환경변수가 변경되면 캐시 무효화
"env": ["NEXT_PUBLIC_API_URL", "NODE_ENV"]
}
},
// 모든 태스크에 영향을 주는 환경변수
"globalEnv": ["DATABASE_URL"]
}
설명
이것이 하는 일: 모노레포의 환경변수 관리 시스템은 각 앱이 독립적인 환경 설정을 가지면서도, 변경 사항을 빌드 캐싱에 정확히 반영합니다. 첫 번째로, 각 앱의 디렉토리에 .env.local 파일을 배치하면 해당 앱에서만 환경변수에 접근할 수 있습니다.
Next.js의 경우 NEXT_PUBLIC_ 접두사가 붙은 변수는 브라우저에 노출되고, 그렇지 않은 변수는 서버 사이드에서만 사용됩니다. DATABASE_URL 같은 민감한 정보는 절대 NEXT_PUBLIC_을 붙이지 말아야 합니다.
.env.production은 프로덕션 빌드 시에만 사용되며, CI에서 실제 값을 환경변수로 주입합니다. 그 다음으로, turbo.json의 env 배열은 해당 태스크의 캐시 키에 환경변수를 포함시킵니다.
예를 들어 NEXT_PUBLIC_API_URL이 http://localhost:3001에서 https://api.staging.com으로 변경되면, 같은 코드라도 다른 해시가 생성되어 캐시가 무효화됩니다. 이렇게 하면 환경변수 변경 시 자동으로 재빌드되어, 잘못된 환경변수로 빌드된 캐시를 사용하는 버그를 방지합니다.
마지막으로, globalEnv는 모든 패키지에 영향을 주는 환경변수를 지정합니다. DATABASE_URL이 변경되면 전체 모노레포의 캐시가 무효화되므로, 데이터베이스 마이그레이션 후에 모든 앱이 올바르게 재빌드됩니다.
이 설정이 없으면 환경변수가 변경되어도 캐시를 재사용하여 예상치 못한 버그가 발생할 수 있습니다. 여러분이 이 패턴을 사용하면 각 앱이 독립적인 환경 설정을 가지면서도 관리 부담은 줄어듭니다.
예를 들어 web 앱은 외부 고객용 API를 사용하고, admin 앱은 내부 관리자용 API를 사용하는 멀티 테넌트 구조도 쉽게 구현할 수 있습니다. 또한 .env.example 파일을 각 앱에 제공하여, 새로운 팀원이 어떤 환경변수가 필요한지 바로 알 수 있습니다.
CI/CD에서도 환경변수 누락으로 인한 배포 실패를 사전에 방지할 수 있습니다.
실전 팁
💡 .env.local은 .gitignore에 추가하고, .env.example은 커밋하여 팀원들에게 필요한 환경변수 목록을 공유하세요.
💡 dotenv-cli를 사용하면 turbo run build --env-file=.env.staging처럼 동적으로 환경변수 파일을 선택할 수 있습니다.
💡 Vercel에 배포할 때는 대시보드에서 환경변수를 설정하면 자동으로 각 앱에 주입됩니다. Preview, Production 환경별로 다른 값을 설정할 수 있습니다.
💡 t3-env 같은 라이브러리를 사용하면 환경변수를 Zod로 검증하여, 런타임에 잘못된 환경변수로 인한 에러를 사전에 방지할 수 있습니다.
10. 배포 전략
시작하며
여러분이 모노레포에서 여러 앱을 배포할 때 이런 고민을 해본 적 있나요? packages/ui를 수정했을 때 모든 앱을 다시 배포해야 할지, 아니면 ui를 사용하는 앱만 배포하면 될지 판단하기 어렵고, 불필요한 배포로 시간을 낭비하는 상황 말이죠.
이런 문제는 변경 감지와 배포를 자동화하지 않았을 때 발생합니다. 수동으로 어떤 앱이 영향을 받는지 추적하면 실수가 발생하고, 배포가 누락되거나 불필요하게 실행됩니다.
특히 여러 팀이 동일한 모노레포를 사용하면 조율이 더욱 복잡해집니다. 바로 이럴 때 필요한 것이 Turborepo의 필터링과 Vercel의 자동 배포 전략입니다.
변경된 패키지를 자동으로 감지하여, 영향을 받는 앱만 선택적으로 배포할 수 있습니다.
개요
간단히 말해서, 모노레포의 배포 전략은 turbo run을 필터링하여 변경된 패키지와 그것에 의존하는 앱만 빌드하고 배포하는 방식입니다. 이 전략은 배포 시간과 비용을 최소화하면서도 안정성을 유지합니다.
예를 들어, packages/utils만 수정했다면 그것을 사용하는 apps/web과 apps/admin만 배포하고, packages/ui를 사용하지 않는 apps/blog는 배포하지 않습니다. 기존에는 모든 앱을 항상 배포하거나, 수동으로 배포 대상을 선택했다면, 이제는 git diff와 Turborepo의 필터링을 조합하여 자동으로 결정합니다.
Vercel의 Ignored Build Step 기능을 사용하면 변경이 없는 앱의 배포를 자동으로 건너뜁니다. 배포 전략의 핵심 기술은 세 가지입니다: turbo run build --filter를 사용하여 특정 앱만 빌드하고, Git의 변경 감지로 영향받는 패키지를 찾으며, Vercel이나 GitHub Actions에서 조건부 배포를 설정합니다.
이러한 기술들이 효율적이고 안전한 배포 파이프라인을 구축합니다.
코드 예제
// package.json - 필터링을 사용한 빌드 스크립트
{
"scripts": {
// 특정 앱만 빌드
"build:web": "turbo run build --filter=@my-company/web",
// web과 그것이 의존하는 모든 패키지 빌드
"build:web:deps": "turbo run build --filter=@my-company/web...",
// 변경된 패키지만 빌드
"build:changed": "turbo run build --filter=[HEAD^1]"
}
}
// vercel.json - Vercel 배포 설정
{
"buildCommand": "turbo run build --filter=@my-company/web",
"ignoreCommand": "npx turbo-ignore @my-company/web"
}
// GitHub Actions - 조건부 배포
// .github/workflows/deploy.yml
- name: Check changes
id: changes
run: |
if turbo run build --dry --filter=@my-company/web...[HEAD^1]; then
echo "deploy=true" >> $GITHUB_OUTPUT
fi
- name: Deploy to Vercel
if: steps.changes.outputs.deploy == 'true'
run: vercel deploy --prod
설명
이것이 하는 일: Turborepo의 필터링 기능은 의존성 그래프를 활용하여, 코드 변경이 어떤 앱에 영향을 주는지 자동으로 계산하고 배포를 최적화합니다. 첫 번째로, --filter 플래그는 다양한 방식으로 패키지를 선택할 수 있습니다.
--filter=@my-company/web은 web 앱만 선택하고, --filter=@my-company/web...은 web과 그것이 의존하는 모든 패키지(packages/ui, packages/utils 등)를 함께 선택합니다. 이렇게 하면 의존성이 먼저 빌드된 후 앱이 빌드되어 순서가 보장됩니다.
--filter=[HEAD^1]은 Git의 마지막 커밋에서 변경된 파일을 가진 패키지만 선택합니다. 그 다음으로, Vercel의 turbo-ignore 명령어는 배포 여부를 자동으로 결정합니다.
vercel.json의 ignoreCommand에 npx turbo-ignore @my-company/web을 설정하면, Vercel이 빌드 전에 이 명령어를 실행합니다. turbo-ignore는 내부적으로 git diff와 의존성 그래프를 분석하여, web 앱이나 그것이 의존하는 패키지에 변경이 있으면 0을 반환(배포 진행)하고, 변경이 없으면 1을 반환(배포 건너뜀)합니다.
마지막으로, GitHub Actions에서 조건부 배포를 구현하는 방법입니다. turbo run build --dry --filter=...는 실제 빌드 없이 실행 계획만 출력합니다.
이 명령어가 성공하면(빌드할 게 있으면) deploy=true를 출력하고, 다음 스텝에서 if 조건으로 확인하여 배포를 실행합니다. 이렇게 하면 변경이 없는 앱은 배포를 건너뛰어 CI 시간과 배포 비용을 절약합니다.
여러분이 이 배포 전략을 사용하면 불필요한 배포가 완전히 사라집니다. 예를 들어 README.md만 수정한 커밋은 어떤 앱도 배포하지 않고, packages/ui를 수정하면 ui를 사용하는 앱만 자동으로 배포됩니다.
이렇게 하면 배포 시간이 크게 단축되고, 각 앱의 배포 이력도 명확해져 롤백이 쉬워집니다. 또한 Vercel의 Preview 배포와 조합하면 PR마다 영향받는 앱만 미리보기를 생성하여, 리뷰 효율성도 높아집니다.
실전 팁
💡 --filter='...[origin/main]'를 사용하면 main 브랜치와 비교하여 변경된 패키지를 찾을 수 있습니다. PR 빌드에 유용합니다.
💡 각 앱을 별도의 Vercel 프로젝트로 만들고, 같은 Git 저장소를 연결하되 Root Directory를 각각 apps/web, apps/admin으로 설정하세요.
💡 --scope 대신 --filter를 사용하세요. --scope는 deprecated되었고, --filter가 더 강력한 기능을 제공합니다.
💡 pnpm deploy 명령어를 사용하면 특정 앱과 그 의존성만 node_modules와 함께 별도 폴더로 추출할 수 있어, Docker 이미지 크기를 최소화할 수 있습니다.
💡 turbo-ignore의 출력을 로그로 남기면, 왜 배포가 건너뛰어졌는지 또는 실행되었는지 추적할 수 있어 디버깅에 유용합니다.
댓글 (0)
함께 보면 좋은 카드 뉴스
마이크로서비스 배포 완벽 가이드
Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.
Application Load Balancer 완벽 가이드
AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.
고객 상담 AI 시스템 완벽 구축 가이드
AWS Bedrock Agent와 Knowledge Base를 활용하여 실시간 고객 상담 AI 시스템을 구축하는 방법을 단계별로 학습합니다. RAG 기반 지식 검색부터 Guardrails 안전 장치, 프론트엔드 연동까지 실무에 바로 적용 가능한 완전한 시스템을 만들어봅니다.
에러 처리와 폴백 완벽 가이드
AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.
AWS Bedrock 비용 최적화 완벽 가이드
AWS Bedrock을 활용한 AI 서비스 개발 시 발생하는 비용을 효과적으로 관리하고 최적화하는 실전 전략을 다룹니다. 토큰 사용량 관리부터 프롬프트 압축, 캐싱 전략까지 실무에서 바로 적용할 수 있는 구체적인 기법들을 소개합니다.