
개발을 시작할 때 제일 먼저 고민하는 것 중 하나가 바로 프로젝트 구조입니다.
코드가 커질수록 "이걸 한 저장소에 다 넣어아 하나?" "아니면 프로젝트별로 따로 관리해야 하나?" 하는 고민이 생깁니다.
이 글에서는 그런 고민을 정리해보려 합니다.
1. 멀티레포에서 시작한 이유
처음에는 각 프론트엔드 앱을 따로 관리하는 멀티레포 구조를 선택했습니다.
각 앱이 독립적으로 배포되어야 했고, 기능도 달랐기 때문입니다.
하지만 곧 여러 문제가 발생했습니다.
공통 UI 컴포넌트의 중복 관리
project1/src/components/Button.tsx
project2/src/components/Button.tsx
project3/src/components/Button.tsx
- 같은 Button, Modal, Table 등의 컴포넌트를 모든 저장소에 복사
- 수정할 때마다 3~4개 저장소를 전부 업데이트
→ 한 곳에서 관리할 수 없다는 점이 큰 비효율이었습니다.
스타일 시스템 불일치
공통 색상, 폰트, 간격, spacing 등이 각 저장소에 제각각 정의되어 있었습니다.
- 버튼 색을 바꿔야할 때 4개 저장소 모두 수정
- 디자인 토큰이 일관되지 않아 UI 차이 발생
→ 스타일의 일관성 유지가 거의 불가능했습니다.
타입, 상수, 유틸 함수 중복
// 공통 API 타입 정의
type ApiResponse<T> = {
data: T;
message: string;
status: number;
}
- API 타입, 유틸 함수, 상수를 복사해 각 저장소에 똑같이 복붙
- 수정 시 모든 저장소를 동기화해야 함
→ 코드 일관성 유지가 어려웠습니다.
의존성 버전 혼란
- React, TypeScript, Vite 버전이 앱마다 달라짐
- 한쪽에서 업데이트하면 다른 앱에서 빌드 에러 발생
- 보안 패치 적용 시 모든 저장소를 일일이 수정해야 함
2. 모노레포로 전환
위 문제를 해결하기 위해 pnpm workspace 기반 모노레포 구조로 전환하였습니다.
✅ 프로젝트 구조 예시
project/
├── apps/
│ ├── project1/
│ ├── project2/
│ └── project3/
├── packages/
│ ├── common_ui/
│ ├── common_utils/
│ ├── common_constants/
│ ├── common_styles/
│ └── common_types/
└── package.json
- apps/: 실제로 배포되는 서비스들
- packages/: 여러 앱에서 공통으로 사용하는 패키지들
✅ 루트 설정 (pnpm workspace)
// package.json
{
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}
✅ 공통 패키지 예시
// packages/common_ui/src/components/Button.tsx
export const Button = ({ size: 'medium', children }) => {
return (
<button className={`btn btn-${size}`}>
{children}
</button>
);
};
✅ 공통 타입 정의 공유
// packages/common_types/src/types/CommonTypes.ts
export type ApiResponse<T> = {
data: T;
message: string;
status: number;
};
// apps/project1/src/api/useUser.ts
import { ApiResponse } from '@project/common_types';
export const getUser = async (): Promise<ApiResponse<User>> => {
// ...
};
✅ 통합 빌드 및 테스트
// 루트 package.json
{
"scripts": {
"build": "pnpm --parallel build",
"test": "pnpm --recursive test",
"project1:build": "pnpm --filter @project/project1 build",
"common:build": "pnpm --filter @project/common_ui build"
}
}
- 필요한 앱만 선택 빌드 가능, 전체 빌드도 한번에 가능
✅ 실제 사용 예시
// apps/project1/src/components/SomeComponent.tsx
import { Button } from '@project/common_ui';
import { formatDate } from '@project/common_utils';
import { DATE_FORMATS } from '@project/common_constants';
import '@project/common_styles';
const MyComponent = () => {
const date = formatDate(new Date(), DATE_FORMATS.YYYY_MM_DD);
return <Button size="medium">{date}</Button>;
};
- 모든 앱이 동일한 인터페이스와 스타일로 동작함
3. 도입 후 얻은 효과 및 결론
| 항목 | 도입 전 (멀티레포) | 도입 후 (모노레포) |
| 공통 코드 관리 | 중복, 수동 반영 | 한 곳에서 수정 즉시 반영 |
| 디자인 시스템 | 앱마다 불일치 | 통일된 스타일 시스템 |
| 타입 정의 | 복사-붙여넣기 | 패키지로 공유 |
| 의존성 관리 | 버전 충돌 잦음 | 단일 버전 유지 |
| 빌드/테스트 | 개별 실행 | 통합 및 선택적 실행 |
| 협업 | 각자 다른 저장소 | 하나의 환경에서 공동 개발 |
결론
1. 앱 간 공통 코드 공유가 필수였다.
2. 디자인 시스템을 일관되게 유지해야 했다.
3. 모든 앱을 같은 팀이 관리했다.
이런 조건이라면 모노레포가 훨씬 효율적입니다.
반대로 팀이 나뉘어 있고 기술 스택이 다르면 멀티레포가 더 나을 수 있습니다.
참고