모노레포 vs 멀티레포, 외주 프로젝트에서의 선택

개발
·Dante Chun

레포지토리 구조 고민

외주 프로젝트를 시작할 때 가장 먼저 결정해야 하는 것 중 하나가 레포지토리 구조다. 프론트엔드, 백엔드, 관리자 페이지... 이걸 하나의 레포에 넣을까, 따로 관리할까?

처음에는 "그냥 편한 대로 하면 되지"라고 생각했다. 하지만 몇 번의 시행착오를 겪고 나니, 상황에 따른 명확한 기준이 생겼다.

멀티레포의 문제점

처음에는 멀티레포로 시작했다

프로젝트 A
├── project-a-frontend/     (별도 레포)
├── project-a-backend/      (별도 레포)
└── project-a-admin/        (별도 레포)

각각 독립적으로 관리하면 깔끔할 것 같았다. 하지만 현실은 달랐다.

문제 1: 타입 공유의 지옥

프론트엔드와 백엔드에서 같은 타입을 써야 하는데, 레포가 다르니 복사-붙여넣기를 해야 했다.

// project-a-backend/src/types/user.ts
interface User {
  id: string
  email: string
  name: string
}

// project-a-frontend/src/types/user.ts
// 복사해서 붙여넣기... API 스펙 바뀔 때마다 수동 동기화
interface User {
  id: string
  email: string
  name: string
}

API 스펙이 바뀔 때마다 양쪽을 수정해야 했다. 당연히 실수가 생겼고, 런타임에서야 발견됐다.

문제 2: 의존성 버전 불일치

// project-a-frontend/package.json
"dependencies": {
  "axios": "^1.4.0",
  "date-fns": "^2.30.0"
}

// project-a-backend/package.json
"dependencies": {
  "axios": "^0.27.0",  // 왜 다르지?
  "date-fns": "^2.28.0"
}

시간이 지나면 의존성 버전이 제각각이 된다. 보안 패치를 적용할 때마다 모든 레포를 돌아다녀야 했다.

문제 3: 배포 동기화

프론트엔드와 백엔드를 동시에 배포해야 하는데, 레포가 다르니 각각 따로 배포해야 했다. CI/CD 파이프라인도 따로 관리.

하나를 배포하고 다른 하나를 까먹으면 버그가 발생했다.

모노레포로 전환

구조 변경

project-a/
├── packages/
│   ├── frontend/
│   ├── backend/
│   ├── admin/
│   └── shared/          # 공유 코드
├── package.json
├── pnpm-workspace.yaml
└── turbo.json

타입 공유 문제 해결

// packages/shared/src/types/user.ts
export interface User {
  id: string
  email: string
  name: string
}

// packages/frontend/src/api/user.ts
import { User } from "@project-a/shared"

// packages/backend/src/api/user.ts
import { User } from "@project-a/shared"

타입을 한 곳에서 정의하고, 양쪽에서 import. API 스펙이 바뀌면 shared만 수정하면 된다. TypeScript가 양쪽에서 타입 에러를 잡아준다.

의존성 통합

# pnpm-workspace.yaml
packages:
  - packages/*

# 루트 package.json
"devDependencies": {
  "typescript": "^5.0.0",
  "eslint": "^8.0.0"
}

공통 의존성은 루트에서 관리. 개별 패키지 의존성만 각각의 package.json에.

통합 배포

# .github/workflows/deploy.yml
jobs:
  deploy:
    steps:
      - name: Deploy Backend
        run: pnpm --filter backend deploy
      - name: Deploy Frontend
        run: pnpm --filter frontend deploy

하나의 워크플로우에서 순차적으로 배포. 실수로 하나만 배포하는 일이 없어졌다.

모노레포 도구 선택

Turborepo를 선택한 이유

모노레포 도구는 여러 가지가 있다:

  • Nx
  • Lerna
  • Turborepo
  • Yarn/pnpm Workspaces

나는 Turborepo + pnpm 조합을 사용한다.

Turborepo 장점:

  • 설정이 간단하다
  • 빌드 캐싱이 빠르다
  • Vercel과 통합이 좋다
  • 학습 곡선이 낮다
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/", "dist/"]
    },
    "dev": {
      "cache": false
    },
    "lint": {},
    "test": {}
  }
}

pnpm Workspaces

# pnpm-workspace.yaml
packages:
  - packages/*
  - apps/*

패키지 간 의존성 설치:

pnpm add @project-a/shared --filter frontend

외주 프로젝트에서의 고려사항

인수인계

모노레포의 큰 장점은 인수인계가 쉽다는 것이다.

❌ 멀티레포 인수인계
"프론트는 이 레포고, 백엔드는 저 레포고, 어드민은 또 다른 레포입니다.
각각 .env 설정이 다르고, 배포 방법도 다릅니다."

✅ 모노레포 인수인계
"이 레포 하나 클론하시면 됩니다.
pnpm install 후 pnpm dev로 모든 서비스가 실행됩니다."

클라이언트나 후임 개발자에게 프로젝트를 넘길 때, 레포가 하나면 설명할 것도 적고 실수할 여지도 적다.

권한 관리

하지만 때로는 멀티레포가 필요한 경우도 있다. 클라이언트가 프론트엔드만 접근하길 원하거나, 보안상 백엔드 코드를 분리해야 할 때.

상황: 외부 프론트엔드 개발자 협업
→ 프론트엔드만 별도 레포로 분리
→ 나머지는 모노레포 유지

클라이언트 요구사항

"프론트엔드는 저희 서버에 배포하고, 백엔드는 귀사 서버에서 운영해주세요."

이런 요구사항이 있으면 배포 환경이 완전히 달라지므로 멀티레포가 나을 수 있다. 하지만 코드 공유는 여전히 필요하니, shared 패키지만 npm에 배포하는 방법도 있다.

내 기준 정리

모노레포 선택

  • 프론트/백엔드를 모두 내가 개발할 때
  • 타입 공유가 중요할 때
  • 통합 배포가 필요할 때
  • 인수인계를 단순화하고 싶을 때

멀티레포 선택

  • 여러 개발자/팀이 독립적으로 작업할 때
  • 권한 분리가 필요할 때
  • 배포 환경이 완전히 다를 때
  • 클라이언트가 특정 부분만 접근해야 할 때

실제 프로젝트 구조 예시

현재 운영 중인 프로젝트 구조:

dante-company/
├── apps/
│   ├── web/              # 메인 웹사이트
│   ├── blog/             # 블로그 (지금 보는 이 사이트)
│   └── admin/            # 관리자 페이지
├── packages/
│   ├── ui/               # 공유 UI 컴포넌트
│   ├── config/           # ESLint, TS 설정
│   └── types/            # 공유 타입
├── pnpm-workspace.yaml
└── turbo.json

모든 프로젝트가 같은 디자인 시스템을 공유하고, 타입도 공유한다. 새 프로젝트를 시작할 때 apps/ 아래에 추가하면 된다.

결론

모노레포 vs 멀티레포는 "뭐가 더 좋다"의 문제가 아니다. 상황에 따라 적절한 선택이 있을 뿐이다.

1인 개발자가 외주 프로젝트를 진행할 때는 모노레포가 대부분의 상황에서 유리하다. 타입 공유, 의존성 관리, 인수인계 모든 면에서.

다만 프로젝트 규모가 커지거나, 팀이 커지거나, 권한 분리가 필요하면 그때 멀티레포로 분리해도 늦지 않다. 모노레포에서 멀티레포로 분리하는 건 그 반대보다 쉬우니까.