프론트엔드

프로젝트 통합으로 생산성 높이기: Turborepo 도입 및 모노레포 마이그레이션 여정

김민나 2026. 2. 22. 22:59

안녕하세요, IT 연합 동아리 Cotato(코테이토) 리뉴얼 프로젝트 프론트엔드 개발로 참여 중인 김민아입니다. 

 

저희 팀은 최근 코테이토 리뉴얼 프로젝트를 진행하며 큰 구조적 변화를 겪었습니다. 기존에는 메인 홈페이지(cotato.kr)와 리쿠르팅 페이지(recruit.cotato.kr)를 각각 독립적인 멀티 레포지토리로 관리할 계획이었습니다. 하지만 프로젝트가 진행될수록 두 서비스 사이의 강력한 연결 고리를 발견했고, 이를 효율적으로 관리하기 위해 pnpm + Turborepo 기반의 모노레포로 마이그레이션을 진행했습니다.

 

모노레포 전환을 고민 중인 다른 팀들에게 도움이 되길 바라며 기록을 공유합니다.

 

1. 도입 배경: 왜 모노레포여야 했나?

코테이토의 웹 생태계는 크게 두 가지 도메인으로 나뉩니다.

  • 메인 홈페이지: 동아리 소개 및 활동 아카이빙
  • 리쿠르팅 페이지: 신규 기수 지원 및 서류/면접 관리

 

두 사이트는 사용 목적과 맥락이 완전히 다르지만, '코테이토'라는 하나의 브랜드 아이덴티티를 공유합니다. 이 과정에서 멀티 레포 구조는 다음과 같은 한계가 있습니다. 

 

  1. 중복 코드의 늪: 디자인 시스템이 변경되면 두 곳의 코드를 전부 수정해야 하는 번거로움
  2. 파편화된 설정: ESLint, Prettier, TypeScript 설정이 미세하게 달라지면서, 협업 시 코드 스타일 컨벤션을 유지하기 어려움
  3. 비효율적인 워크플로우: 두 프로젝트를 동시에 수정해야 할 때 브랜치를 각각 생성하고, 레포지토리를 오가며 컨텍스트 스위칭 비용 증가

 

우리는 "의미적으로 연결된 프로젝트를 하나로 묶어 관리하자"는 목표 아래 마이그레이션을 결정했습니다.

 

코테이토 리쿠르팅 사이트
코테이토 메인 홈페이지


2. 기술 스택 선정: pnpm & Turborepo

효율적인 모노레포 구축을 위해 pnpmTurborepo 조합을 선택했습니다.

 

  • pnpm (Workspace): 효율적인 의존성 관리와 빠른 설치 속도를 제공합니다. 특히 workspace:* 프로토콜을 통해 내부 패키지 간의 참조를 명확하게 관리할 수 있습니다.
  • Turborepo: 빌드 캐싱과 파이프라인 최적화를 통해 모노레포의 단점인 늘어나는 빌드 시간을 획기적으로 줄여줍니다.

3. 마이그레이션 전략 및 실행

 

3.1 구조 설계 (Directory Structure)

apps에는 실제 서비스되는 웹 앱을, packages에는 모든 앱이 공유하는 설정과 컴포넌트를 배치했습니다. 특히 Tailwind 설정을 패키지화하여 디자인 시스템의 일관성을 강제했습니다.
.
├── apps/
│   ├── homepage (cotato.kr)
│   └── recruit  (recruit.cotato.kr)
├── packages/
│   ├── ui                # 디자인 시스템 & 공통 컴포넌트 관리
│   ├── eslint-config     # 공통 린트 설정
│   └── typescript-config # 공통 타입스크립트 설정
│   └── tailwind-config # 공통 디자인 시스템 설정
├── turbo.json
└── pnpm-workspace.yaml

 

3.2 Git 히스토리 살리기 (Git Subtree)

 

마이그레이션 시 가장 우려했던 점은 기존 recruit 레포의 커밋 히스토리가 사라지는 것이었습니다.

저희는 Git Subtree 방식을 사용하여 기존 이력을 유지한 채 apps/recruit으로 성공적으로 이관했습니다.

 

Git Subtree 이용 방법

git subtree add --prefix=apps/recruit [기존레포URL] [브랜치명]

 

3.3 공유 패키지(Shared Package) 분리 원칙

 

모노레포의 핵심은 공유이지만 무분별한 공유는 의존성 스파게티를 만듭니다. 따라서 저희는 다음 원칙을 세웠는데요,

 

  1. 순수 UI만 포함: packages/ui에는 비즈니스 로직이 없는 디자인 시스템 레벨의 컴포넌트만 둡니다.
  2. 단방향 의존성: packages는 apps를 참조할 수 없으며, 오직 apps가 packages를 가져다 씁니다.
  3. 명확한 경로: 상대 경로(../../ui) 대신 @repo/ui와 같은 절대 경로 임포트만 허용합니다.

 

3.4 모노레포 생성하기

새로운 레포지토리를 생성하고, pnpm과 Turborepo를 이용해 기본 뼈대를 구성하는 구체적인 단계입니다.

1) Turborepo 기본 구조 생성

먼저 터미널에서 아래 명령어를 실행하여 pnpm 기반의 Turborepo 템플릿을 생성합니다.

 

npx create-turbo@latest
  • Package Manager: pnpm 선택
  • Directory: 프로젝트 이름 설정 (예: cotato-monorepo)

2) pnpm-workspace.yaml 설정

루트 디렉토리에 pnpm-workspace.yaml 파일을 작성하여 패키지 경로를 정의합니다.

packages:
  - 'apps/*'
  - 'packages/*'

3) 기존 리쿠르팅 레포지토리 이관 (Git Subtree)

기존 recruit 레포지토리의 커밋 히스토리를 유지하며 apps/recruit 폴더로 가져옵니다.

# 원격 저장소 추가
git remote add recruit-origin [기존_리쿠르팅_레포_URL]

# subtree를 이용해 특정 폴더로 코드 가져오기
git subtree add --prefix=apps/recruit recruit-origin [브랜치명]

 

4) 의존성 설치 및 로컬 패키지 연결

 

각 앱에서 공통 패키지(UI, Config 등)를 사용할 수 있도록 의존성을 주입합니다. 예를 들어 apps/recruit에서 공통 UI를 사용하려면 다음과 같이 설정합니다. (Config 파일은 각 프로젝트의 규칙에 맞게 작성합니다.) 

 

apps/recruit/package.json

{
  "dependencies": {
    "@repo/ui": "workspace:*"
  }
}

이후 루트에서 의존성을 설치합니다.

pnpm install

 

5) 새로운 홈페이지(Homepage) 앱 생성

apps/ 디렉토리 내부에 Next.js 등 원하는 프레임워크를 설치하여 개발을 시작합니다.

cd apps
npx create-next-app@latest homepage

 


4. 모노레포 운영 규칙

통합된 환경에서 여러 개발자가 혼선 없이 작업하기 위해 새로운 컨벤션을 수립했습니다.

 

4.1 독립적인 실행과 포트 고정

전체 빌드 시 리소스를 아끼기 위해 --filter 옵션을 적극 활용합니다. 또한 로컬 개발 시 충돌을 방지하기 위해 포트를 고정했습니다.

 

  • Recruit: 3000 포트 (pnpm dev --filter recruit)
  • Homepage: 3001 포트 (pnpm dev --filter homepage)

 

4.2 브랜치 & PR 컨벤션 전략

브랜치 생성 시 root / recruit / homepage 작업 중 어느 작업을 진행하는지를 명시하고, 깃허브의 레이블 기능을 활용해 팀원들끼리 작업 공유를 수월하게 할 수 있도록 진행했습니다.

 

  • root 기반 작업 : feat/root/이슈번호-기능명세
  • recruit 기반 작업 : feat/recruit/이슈번호-기능명세
  • homepage 기반 작업 : feat/homepage/이슈번호-기능명세 

 

또한 모든 PR, 이슈 작성에는 레이블을 명세하여 어떤 작업을 진행했는지 시각적으로 명시했습니다. 

 


5. 마이그레이션 중 마주한 고민들

 

고민 1: 공통 컴포넌트 분리의 경계 설정

 

어디까지를 packages/ui로 분리할 것인가에 고민이 있었는데요, 모든 컴포넌트를 공유하려다 보면 오히려 패키지 간 결합도가 높아져 유지보수가 어려워질 수 있기 때문입니다.

 

저희는 비즈니스 로직을 포함하지 않는 순수 UI 컴포넌트 위주로 이관한다는 원칙을 세웠습니다. 특정 도메인에 종속된 컴포넌트는 각 apps 내부에 유지하고, 디자인 시스템의 최소 단위가 되는 컴포넌트들만 공유 패키지로 관리함으로써 독립성을 보장했습니다. 

 

공통 컴포넌트 분리 경계 설정에 있어서는 아직 고민 중인 부분이 많으므로, 각 팀에 맞게 원칙을 세우는 것이 필요할 것 같습니다. 

 

고민 2: 점진적 이관

 

한번에 모든 코드를 packages/ui로 옮기는 것은 위험합니다.

중복도가 높고 안정적인 컴포넌트부터 단계적으로 분리하며 매 단계마다 배포 환경 검증을 거쳤습니다.


6. 마치며

모노레포 도입은 팀 전체가 "기술 스택의 표준화"를 경험하고, 코드 재사용에 대해 더 깊이 고민하게 되는 계기가 되었습니다.

 

모노레포 전환을 고민 중인 다른 팀에게 이 글이 도움이 되길 바라면서 글을 마칩니다! 


참고한 아티클: