모노레포가 모노?

전준연·2025년 2월 1일
post-thumbnail

목적

최근 팀의 레포지토리를 살펴보다가 의존성과 관련해 개선할 수 있는 부분을 발견하여 같은 팀 선배님들께 "이 부분을 개선해볼 수 있지 않을까요?"라고 질문을 드렸다. 선배님께서 잘 발견했다며, 가능하다면 직접 수정해보라고 하셨다. 하지만 프로젝트가 내가 익숙한 구조가 아니라 모노레포 구조로 되어 있어 쉽게 건드리지 못했다. 그래서 모노레포에 대해 공부해보려던 차에, 마침 선배님께서 모노레포에 대해 공부하고 정리하라는 과제를 주셔서 이 글에서 모노레포에 대해 정리해보려고 한다.

모노레포?

모노레포(Monolithic Repository)란 잘 정의된 관계를 가진 여러 개의 독립적인 프로젝트를 하나의 레포지토리에서 함께 관리하는 방식을 말한다. 일반적으로 각 프로젝트마다 별도의 레포지토리를 사용하는 멀티레포(Multi Repo) 방식이 많이 쓰이지만, 최근에는 코드 재사용성과 효율적인 의존성 관리 등의 장점 때문에 모노레포가 점점 더 많이 활용되고 있다고 한다.

장점

  1. 코드 재사용성 향상

    기존의 멀티레포 방식은 동일한 코드를 사용하기 위해 복사/붙여넣기가 필요했다. 하지만 모노레포를 사용하면 공통 코드를 한 곳에서 관리할 수 있어, 코드 변경 시 여러 레포지토리를 수정하는 번거로움이 줄어든다.

  2. 의존성 관리가 용이

    모노레포를 사용하면 여러 프로젝트에서 공통으로 사용하는 라이브러리나 도구를 한 곳에서 관리할 수 있다. 각 프로젝트별로 개별적으로 의존성을 업데이트하거나 테스트할 필요 없이 일관된 환경을 유지할 수 있다.

  3. 통일된 개발 환경

    모노레포를 사용하면 모든 프로젝트가 동일한 ESLint, Prettier, TypeScript 설정 등을 공유할 수 있다. 이를 통해 팀원 간 코드 스타일 차이를 줄이고, 개발 생산성을 높일 수 있다.

단점..

  1. 빌드와 테스트 속도 저하

    모노레포는 여러 프로젝트가 하나의 레포지토리에 포함되어 있어, 빌드 및 테스트 시간이 길어질 수 있다. 특히 프로젝트 규모가 커질수록 이 문제가 더욱 두드러진다.

  2. 레포지토리 크기 증가

    시간이 지나면서 레포지토리 크기가 점점 커지면, clone이나 pull 시 시간이 오래 걸릴 수 있다. 이로 인해 저장소 관리 자체가 부담스러워질 수도 있다.

  3. 특정 프로젝트만 분리하여 관리하기 어려움

    여러 프로젝트가 하나의 레포지토리에 포함되어 있기 때문에, 특정 프로젝트만 독립적으로 배포하거나 업데이트하는 것이 복잡할 수 있다.

멀티레포랑 무슨 차이?

비교할 내용이 많아서 간단히 모노레포와 멀티레포의 차이점을 표로 정리해봤다.

모노레포 (Mono Repo)멀티레포 (Multi Repo)
레포지토리 구조여러 프로젝트를 하나의 레포지토리에서 관리프로젝트별로 각각의 레포지토리에서 관리
코드 공유공통 코드를 쉽게 공유 가능 (패키지, 라이브러리 등)공통 코드 공유가 어렵고, 각 레포에서 중복될 가능성이 높음
의존성 관리프로젝트 간 공통 의존성을 한 곳에서 통합 관리 가능각 프로젝트에서 개별적으로 의존성을 관리해야 함
CI/CD전체 프로젝트를 한 번에 빌드 및 배포 가능 (도구 사용 시 최적화 필요)개별 프로젝트마다 독립적인 빌드 및 배포 설정이 필요
버전 관리한 번의 커밋/PR로 여러 프로젝트에 영향을 줄 수 있음프로젝트별로 버전을 독립적으로 관리 가능
레포지토리 크기시간이 지날수록 커질 가능성이 높음개별 프로젝트의 크기만 관리하면 됨
개발 환경ESLint, Prettier, TypeScript 등의 설정을 통일 가능프로젝트마다 다른 설정을 가질 수 있음
협업 방식여러 팀이 같은 레포를 사용하여 긴밀한 협업 가능팀별로 별도의 레포를 사용하여 독립적으로 개발 가능
빌드/배포 속도빌드 및 테스트 시간이 길어질 가능성이 있음프로젝트별로 독립적이므로 필요할 때만 빌드 및 배포 가능
사용 사례대규모 프로젝트, 라이브러리 공유가 필요한 환경 (Google, Facebook, Microsoft 등)작은 규모의 독립적인 서비스 (스타트업, 특정 기능별 프로젝트)

언제 쓰면 좋을까?

모노레포를 사용하면 얻을 수 있는 장점이 많지만, 개인적으로는 앞서 말했던 장점들, 여러 프로젝트에서 공통 코드나 라이브러리를 공유해야 하거나, 통일된 개발 환경(ESLint, Prettier, TypeScript 설정 등)이 필요하거나, 여러 프로젝트 간 의존성을 통합 관리하는 것이 더 효율적이라고 판단될 때 활용하는 것이 가장 적절하다고 생각한다. 이러한 장점들을 극대화할 수 있는 상황이라면, 단점들을 감수하더라도 충분히 사용할 가치가 있다고 본다.

번외

번외로 빼긴 했지만, 사실 과제 내용이다.

현재 Hello, GSM 프로젝트 구조 분석

폴더 구조

hello-gsm-front  
├── apps                   # 개별 애플리케이션 폴더  
│   ├── client               # 사용자용 애플리케이션  
│   ├── admin                # 관리자용 애플리케이션  
│   └── storybook            # UI 컴포넌트 문서화 및 테스트  
├── packages               # 공통 모듈과 설정을 저장하는 폴더  
│   ├── api                  # API 관련 코드 (Axios 인스턴스, API 함수 등)  
│   ├── code-style-config    # ESLint, Prettier 등 코드 스타일 관련 설정  
│   ├── shared               # 공통 유틸 함수, hooks, 또는 컴포넌트  
│   ├── tailwind-config      # Tailwind CSS 설정 공유  
│   └── types                # 공통 타입 정의 (인터페이스 등)  
├── package.json           # 루트 패키지 설정  
├── pnpm-lock.yaml         # 의존성 버전 고정 파일  
├── pnpm-workspace.yaml    # 모노레포 워크스페이스 설정  
├── tsconfig.json          # TypeScript 설정  
└── 기타 파일               # 기타 설정 파일 (.gitignore 등)  

의존성 관리

Root package.json

"dependencies": {
  "@radix-ui/react-slot": "^1.0.2",
  "@storybook/react": "^8.1.6",
  "@tanstack/react-query": "^5.40.1",
  "@types/react": "^18",
  "@types/react-dom": "^18",
  "@typescript-eslint/eslint-plugin": "7.5.0",
  "@typescript-eslint/parser": "7.5.0",
  "axios": "^1.7.2",
  "class-variance-authority": "^0.7.0",
  "clsx": "^2.1.1",
  "eslint": "8.57.0",
  "lucide-react": "^0.394.0",
  "next": "14.2.3",
  "react": "^18",
  "react-dom": "^18",
  "react-hook-form": "^7.52.0",
  "react-toastify": "^10.0.5",
  "tailwind-merge": "^2.3.0",
  "tailwindcss-animate": "^1.0.7",
  "zod": "^3.23.8"
}

Client package.json

"dependencies": {
  "@hookform/resolvers": "^3.7.0",
  "@radix-ui/react-slot": "^1.0.2",
  "api": "workspace:^",
  "eslint-config-custom": "workspace:^",
  "react": "^18",
  "react-daum-postcode": "^3.1.3",
  "react-dom": "^18",
  "react-hook-form": "^7.52.0",
  "shared": "workspace:^",
  "tailwind-config": "workspace:^",
  "types": "workspace:^"
}

Admin package.json

"dependencies": {
  "@radix-ui/react-slot": "^1.0.2",
  "api": "workspace:^",
  "class-variance-authority": "^0.7.0",
  "clsx": "^2.1.1",
  "eslint-config-custom": "workspace:^",
  "lucide-react": "^0.394.0",
  "next": "14.2.3",
  "react": "^18",
  "react-dom": "^18",
  "shared": "workspace:^",
  "tailwind-config": "workspace:^",
  "tailwind-merge": "^2.3.0",
  "tailwindcss-animate": "^1.0.7",
  "types": "workspace:^"
}

Hello, GSM은 pnpm을 활용한 모노레포 구조로 되어 있다. 따라서 root에 설치된 라이브러리 중 불필요하게 각 패키지에서 중복 설치된 항목은 제거하는 것이 좋을 것 같다. 이렇게 하면 의존성 관리가 더욱 효율적으로 되고, 패키지 간 일관성을 유지할 수 있을 것 같다.

의존성 관리 예시

  • react, react-dom 같은 기본적인 라이브러리는 root에 두고 개별 패키지에서는 제거
  • eslint-config-custom 같은 공통 설정 패키지도 root에서 관리하고, 개별 패키지에서 제거

Hello, GSM은 왜 모노레포를 활용했을까?

모든 코드를 완벽히 분석한 것은 아니지만, Hello, GSM이 모노레포를 활용한 이유는 입학 원서 접수라는 메인 서비스를 효율적으로 관리하기 위해서라고 생각한다.

특히, 클라이언트와 어드민 페이지를 같은 코드베이스에서 관리하면 공통 코드(특히 원서 관련 API 요청, 타입 정의 등)를 쉽게 공유할 수 있다. 이를 통해 코드 중복을 줄이고 유지보수를 간편하게 할 수 있기 때문에 모노레포를 활용한 것으로 보인다.

또한, 모노레포를 활용하면 다음과 같은 장점이 있다.

  • 일관된 코드 스타일과 설정을 유지할 수 있다.
  • 개별 애플리케이션 간의 종속성을 쉽게 관리할 수 있다.
  • 공통 패키지를 한 곳에서 관리하여 효율성을 높일 수 있다.

이러한 이유들로 Hello, GSM은 모노레포를 활용하여 개발한 것으로 보인다.

마무리

이번 과제를 하면서 공부도 할 겸 모노레포에 대해 알아봤다. 솔직히 진짜 대규모 프로젝트가 아니라면 직접 활용할 기회는 많지 않을 것 같지만, 그래도 배워서 나쁠 건 없으니 언젠가 활용해볼 일이 있었으면 좋겠다.

2개의 댓글

comment-user-thumbnail
2025년 2월 1일

중요한 내용이 빠진것 같은데 어플리케이션들관의 관계가 가장 중요하다고 생각해요.
어플리케이션들간의 관계가 형성되지 않으면 의미없는 묶음이 되기 때문에 잘 고려하고 도입해야합니다.

예) hello gsm 같은 client, admin 같은 경우는 원서 접수 서비스라는 목적을 가지고 있기 때문에 모노레포를 도입하여 위에서 말한 장점들을 얻고 있습니다.

1개의 답글