모노레포란?
하나의 레포지토리에서 독립적인 여러 프로젝트를 관리하는 방법

장점
- 빠른 코드 수정
- utilA, utilB를 변경 하더라도 App에 바로 반영
- 멀티 레포의 경우 버전을 올리고 의존성을 다시 설치해야만 사용할 수 있었음
- 각 레포마다 사용했던 같은 코드들의 중복 제거
- 레포 마다 매번 작성던 util, component를 한곳에 관리하여 코드의 중복을 줄이고 생산성을 높일 수 있음
- 수월한 코드 리팩토링
- 코드를 한번에 관리하기 때문에 대규모 리팩토링이 쉬워짐
- 코드 컨벤션 통일
- 멀티 레포에서는 각 레포마다 다른 컨벤션을 가졌다면 모노레포에서는 한곳에서 관리하여 통일 하기 수월함
- eslint 패키지를 만들고 각 페이지에 주입하면 컨벤션 관리에 용이
- 통합 CI, test 관리
- 한꺼번에 ci를 돌릴 수 있고, test를 돌릴 수 있음
- 멀티 레포의 경우 수정 사항이 생기면 각 레포마다 테스트를 돌렸다면, 'yarn test' 명령어 한 번으로 전체 테스트 가능
단점
- 의존성 관리 복잡
- 서로 의존성 연결이 쉽기 때문에 과도한 의존 관계가 생길 수 있음
- A 패키지에 B 패키지를 의존하게 하면 C 패키지에 B 패키지를 명시하지 않아도 B 패키지를 사용할 수 있음 이는 배포할 때 문제를 야기함
- 실제로 이번 프로젝트에서 editor라는 패키지는 design-system을 패키지를 주입할 수 있지만 design-system은 editor를 주입할 수 없음
- 무거운 프로젝트 (CI 속도 저하)
- 하지만 멀티 레포에서는 하나의 변경 사항이 다른 레포의 어떤 영향을 주었을지 모르고, 전부 테스트를 돌려야 했음 모노레포로 옮기면서 오히려 장점같은 단점
- Code ownership 위배
- 작은 팀이 사용한다면 상관 없겠지만 많은 팀이 하나의 레포를 관리한다면 코드 오너쉽을 위배하여 관리 체계가 혼동될 수 있음
모노레포를 선택하게 된 계기
Hops: 개발자를 위한 백오피스, 어드민을 쉽게 만들어내는 솔루션
Hops라는 제품에는 여러 시점이 존재
Hops의 개발자 시점 / Hops로 백오피스를 만드는 개발자 시점 / Hops로 백오피스를 운영하는 운영팀 시점
Hops의 개발자 시점
- Hops 솔루션을 개발하기 위한 UI를 개발해야 함 (App)
- Hops로 개발하는 개발자가 쓸 수 있는 UI를 제공 해주어야 함 (@hops/ui)
- UI는 추후 라이브러리로 배포 될 수 있어야 함
- UI와 App에 공통적으로 디자인 시스템을 적용해야 함 (@hops/design-system)
- 컴파일을 통해 리액트로 코드를 추출 할 수 있어야 함 (@hops/compile)
Hops로 백오피스를 만드는 개발자 시점
- 제공된 UI로 백엔드 로직을 연결해야 함
- 추후 원하면 리액트로 커스텀 개발 해야 함
- 코드를 다운 받아 로컬 개발을 할 수 있어야 함
Hops로 백오피스를 운영하는 운영팀 시점
- Hops를 통해 회사 제품을 운영 해야함
- 백오피스를 수정하지는 못해야 함
결론
- 개발한 UI를 Hops의 개발자 뿐만 아니라 Hops로 백오피스를 만들 고객사의 개발자들도 쓸 수 있어야 한다!
- React로 컴파일을 해서 로컬에서 개발이 가능 하도록 하기 때문.
- 물론 UI만 라이브러리 레포로 만들어서 사용해도 됨
- 하지만 모노레포를 적용함으로써 관리 코스트의 이점을 생각
- 또한 앞으로 공통으로 쓰일 많은 패키지들을 위해 모노레포를 적용하기로 결정
왜 Turborepo인가?


실제로 Turborepo, Nx, Yarn workspace 만들어 봄
- 세팅은 역시 Turborepo, Nx가 빠르다는 인상
- Turborepo가 Next.js Vercel, TypeScript에 친화적이라 쓰고 싶었지만 yarn Plug'n'Play (a.k.a "PnP") 기능을 지원하지 않음
- Nx는 다 해주지만 너무 커다란 프레임워크 느낌
- Yarn workspace 세팅 해봤는데 단독으로 쓰기에는 다른 툴체인에 비해 부족함
결론
아무래도 원래 쓰는 스택을 생각하면 최대한 활용도가 높아보이는 Turborepo 채택!
Turborepo 적용하기
- Next.js / TypeScript / Yarn 사용 (공식에서는 pnpm 추천)
- 설치해보기
yarn dlx create-turbo@latest
(yarn version 3.x.0)
Turborepo 구성 살펴보기
- apps
- apps/docs
- apps/web
- 실제 App 프로덕트 폴더
- 기본 next 프로젝트
- packages
- packages/eslint-config-custom
- eslint 커스텀하여 공통으로 쓸 수 있는 폴더
- packages/tsconfig
- Config 기본으로 설정하는 폴더
- next 설정도 할 수 있음
- packages/ui
- UI 패키지 폴더
- 공통으로 사용되는 UI 컴포넌트를 만들 수 있음
실제 Hops 프로덕트 구조

- 원래는 스토리북이 패키지 별로 있었음
- vercel에 띄우는게 각 패키지 별로 스토리북을 띄워야해서 리소스 낭비로 하나로 통합
- 주요 패키지
- compiler: Hops에서 만든 페이지/쿼리 리소스를 리액트 코드로 컴파일하는 패키지
- core: Hops의 코어 로직을 담당하는 패키지 (직접 만든 StateMachine)
- design-system: 디자인 시스템 패키지
- domain: vo model을 위한 도메인 패키지
- editor: Hops의 코드 editor를 담당하는 패키지 (SQL parser, JS parser 등)
- lexical: Hops의 page editor를 담당하는 패키지 (실제 UI를 불러서 그리는 곳)
- ui: UI 패키지 (라이브러리가 될 곳)
만든 패키지 연동하기
apps/web/package.json

새 패키지 만들어보기
수동 설치
mkdir packages/lib-a
yarn init
Code gen 사용
yarn turbo gen workspace --copy
의존성 그래프 보기
graphviz 설치

해당 명령어는 빌드와 린트에 대한 의존성을 보는 것이기 때문에 빌드만 보고 싶다면 린트는 빼도 상관 없다.

Vercel로 배포하기
-
만약 vercel env를 사용한다면 turbo.json에 추가해주어야 함

-
Root 디렉토리: apps/web으로 설정
-
빌드 세팅은 안해도 됨..인줄 알았으나..
-
이번 프로젝트의 경우 pdkit 의존성 설치를 따로 해주어야 해서 따로 설정을 해주어야 했음
-
이 경우 vercel에서 command override를 제공

Tip 1. filter 사용하기
Root 상황에서 yarn dev, yarn test와 같은 명령어 실행 시 모든 app과 package가 실행 됨.
하나의 모듈만 실행 시키고 싶다면?
yarn dev --filter=web
- 프로젝트를 실행 시킬때 해당 프로젝트 경로로 이동 후 yarn dev해도 되지만 Root 경로에서 yarn dev --filter=web으로 하면 web만 실행 시킬 수 있음
Tip 2. path alias 하기
apps/web에서 import Button from '@hops/ui/Button'을 하고 싶다면?
compilerOptions path 설정하기
"compilerOptions": {
"baseUrl": ".",
"paths: {
"@hops/ui/*": [
"./*"
]
}
}
적용 후 장 / 단점
장점
- 패키지 분리로 인해 공통 로직 관리가 쉬워졌다.
- @hops/core에서 fixture, test에 필요한 util 함수를 만들어서 재사용을 잘 활용하고 있음
- 코드 구조가 한눈에 잘 보인다
- 패키지의 코드 구조를 확인 하고 싶을 때 바로바로 확인이 가능하여 디버깅에 용이함.
- Next, TypeScript와 찰떡콩떡
단점
- 의존성 관리가 힘들다
- 최근에는 root에 모든 패키지를 설치하기로 합의 함
- yarn 1에서는 A패키지의 Z의존성의 버전이 1이었다면 B패키지의 z의존성의 버전이 2인 경우 제대로 나뉘어져서 주입되지 않는 이슈가 있음
- Yarn berry로 올리면서 이슈는 해결 됨
- 패키지 간의 이존성 관계 설계 복잡
- CI나 컴파일러가 막아주는 부분은 없기 때문에 사람이 생각 해야함
- 의존 관계로 만들기 전 구조를 먼저 생각하고 규칙을 정하기로 함
- editor는 design-system을 중비할 수 있지만 design-system은 editor를 주입할 수 없음
- 모든 패키지를 테스트 하기 때문에 CI는 역시 오래 걸린다
- 최근에는 모든 테스트를 돌리지 않고 변동 사항이 있는 부분만 테스트 돌리게 변경함

결론
모노레포는 하나의 제품에 필요한 패키지를 여러개 만들어야 할 때 적절한 대안
이러한 특성 덕분에 이번 프로젝트에서의 모노레포는 아주 적절했던 방안이라 생각
마무리
이번 영상에서는 Turborepo, Next.js, TypeScript 조합으로 모노레포를 적용하는 방법에 대한 내용을 다뤘습니다. 저번 스터디 때 모노레포를 적용해보고 싶다는 분들이 많아서 모노레포가 뭐지? 하고 잠시 찾아본적이 있어서 더 관심있게 들었던 거 같습니다. 확실히 회사의 입장에서 관리해야 하는 repo가 많다면 고려해볼만한 방법인 거 같습니다.
Reference