MonoRepo
제각각의 프로젝트들을 하나로 묶어주어 편하게 작업할 수 있고, 의존관계 관리도 편리하게 도와주는 것이 바로 오늘의 주제인 모노레포 입니다!
지금부터 모노레포 도입 경험을 공유하려고 합니다.
모노레포 도입을 고민한 이유와 모노레포 도입과 적용 후기 순서로 공유하겠습니다! 😁
Monorepo 도입 전, Multi-repo들의 난립
2022년 10월 현재 제가 재직 중인 회사엔 2가지 프로덕트가 존재합니다.
각 프로덕트에는 여러 프론트 프로젝트가 존재하고, 모두 TypeScript + React로 이루어진 프로젝트입니다.
하지만, 프로젝트의 config들은 제각각이어서 코드 컨벤션, 커밋 로그 컨벤션 등이 통일이 안되어 있고 dependency들의 버전들도 모두 달랏습니다.
또한, 프로젝트가 서로 다른 컨벤션으로 계속 커지면서 중복되는 로직과 컴포넌트들은 점점 많아져 관리의 소요는 점점 늘어나는 상황이었습니다. 🤯
(같은 코드인데도 서로 다른 컨벤션으로 이해하는데 시간이 소요되고, 중복된 코드들은 복붙으로 서로의 프로젝트를 넘나드는 상황이 되었습니다. 🤦♀️)
이런 불편함을 해소하고 싶어 작은 것부터 차근차근 해결해 나갔습니다.
먼저 공통 로직들부터 정리하자.
먼저 작은 것부터 정리하기 시작했습니다.
공통되는 로직을 패키지에 묶어 npm private로 배포했습니다.
대표적으로 axios instance를 만들 때, 사내 공통으로 사용되는 config와 interceptor 로직 통합한 후 공통 패키지로 부터 제공하여 각 프로젝트에서 일관된 axios를 사용할 수 있도록 했습니다.
import { camelizeKeys, decamelizeKeys } from "humps";
class Api {
serverUrl: string;
constructor(serverUrl: string) {
this.serverUrl = serverUrl;
// 전사적으로 서버가 snake case를 사용하고 있었기에,
// 이에 맞춰 response시에는 camel case로 request시에는 snake case로 바꿔주는
// interceptor를 공유했습니다.
axios.interceptors.response.use((res) => ({
...res,
data: camelizeKeys(res.data),
}));
axios.interceptors.request.use((config) => {
const newConfig = { ...config };
if (config.params) {
newConfig.params = decamelizeKeys(config.params);
}
if (config.data) {
newConfig.data = decamelizeKeys(config.data);
}
return newConfig;
});
}
}
이렇게 공통 로직들은 조금씩 정리됐지만,
새롭게 만든 패키지 프로젝트를 따로 관리하면서 배포하고
배포에 맞춰 의존 버전을 갱신하고 이에 맞춰 코드를 바꿔주니 굉장히 번거롭고 불편했습니다. 😿
손이 10개라도 모자랄 지경이었죠.
이런 상황에서 모노레포 환경 구축을 생각하게 됩니다!
모노레포 도구 선택
본격적인 도입에 앞서 여러 모노레포 도구들을 알아보았습니다!
- yarn workspace + lerna
- pnpm
- turboRepo, nx
yarn workspace (1.x) + lerna
모노레포하면 대표적으로 사용되는 도구입니다!
여러 프로젝트를 한번에 관리할 수 있고, 패키지 버전 관리와 배포까지 가벼운데 필요한 기능만 알차게 들어있습니다. 또한, 레퍼런스도 풍부하여 채택하였습니다.
yarn workspace와 lerna는 공식 홈페이지에서도 소개하고 있을 정도로 궁합이 좋은 조합입니다!
yarn workspace는 프로젝트의 dependency와 프로젝트간의 dependency를 관리 등의 저수준 관리를, lerna는 버전관리와 배포같은 고수준의 관리를 맡습니다.
pnpm을 선택하지 않은 이유
pnpm은 기존의 npm과 yarn에서 dependency들을 flat하게 관리하기에 일어나는 유령 의존성 등의 문제를 효율적으로 해결한 패키지 매니저입니다.
모노레포 또한 지원하고 있습니다.
하지만, yarn berry로의 업그레이드를 염두해두고 있었기에 채택하지 않았습니다.
yarn berry를 선택하지 않은 이유
yarn berry의 Plug’n’Play의 경우, node_modules가 아닌 다른 곳에 dependency들을 관리하기 때문에, 여러 문제들이 발생합니다.
TypeScript나 eslint, bundler 등에 바뀐 path를 다시 설정해줘야 하는 등의 여러 작업들이 많이 필요합니다.
하지만, 당시 회사가 가진 자원과 시간을 생각했을 때, 모두 해결하기 어려울 것이라고 생각했습니다.
따라서, 우선 성공적으로 yarn (v.1.x) workspace를 도입한 후에 점진적으로 도입해 나갈 계획입니다.
Build System
모노레포를 지원하는 도구 중에 Nx와 TurboRepo는 자신을 Build System이라고 소개하고 있습니다.
모노레포 프로젝트가 커져서 패키지들이 점점 더 많아지고, 하나의 피쳐 개발에도 여러 패키지들을 동시에 개발해야하는 상황이 오면, 관리하기 점점 더 복잡한 상황이 오게 됩니다.
- 빌드에 소요되는 시간이 점점 증가한다.
코드에 양이 커진 만큼 빌드에 소모되는 시간이 기하급수적으로 증가하는데, 이를 Build System이 해결해줍니다.
빌드 최적화를 통한 속도 향상, 캐싱, 분산 빌드 작업 등으로 거대한 빌드 작업도 효율적이고 빠르게 처리할 수 있도록 도와줍니다.
- 코드 변화로 인해 영향 받는 프로젝트들을 파악하기 힘들어진다.
프로젝트간 수많은 의존성이 엮이다보면, 코드 한 줄을 바꿔도 여러 프로젝트들이 영향을 받게 됩니다.
이런 복잡한 상황을 Build System이 알려주고, 의존 관계를 시각적으로도 표현해줍니다.
하지만, 이정도로 고성능의 도구가 필요하지 않았기 때문에 선택하지 않았습니다.
Monorepo 도입
이제 본격적으로 모노레포를 도입합니다!
모노레포에 프로젝트들 병합 및 공통 config 셋팅과 도입하면서 어려웠던 점들을 이야기 해보겠습니다.
기존 프로젝트 병합
기존 프로젝트 병합은 “lerna import”를 이용했습니다.
기존 프로젝트들을 병합하려고 할 때, 모노레포가 새로운 프로젝트이다보니, 각 프로젝트들의 git commit history를 어떻게 가져와야할지 고민이 됐습니다.
프로젝트를 그대로 모노레포안에 복사 붙여넣기 하면, 기존의 가지고 있던 .git과 모노레포의 .git의 충돌이 있어, 그럴 수 없었습니다.
lerna는 이 문제를 해결해줍니다. lerna import를 통해, 기존 프로젝트들이 가지고 있던 git commit history를 모두 가져와 모노레포의 git history에 commit해줍니다.
공통 config 설정
이제 공통 config들을 셋팅할 차례입니다.
eslint
처음엔 모노레포 root에 config들을 두고 하단의 package들에서 config를 override해서 사용하는 것을 생각했었습니다.
하지만 각 project들의 기술 스택이 제각각이어서 공통 config를 두는 것이 비효율적이었습니다.
client는 next를 사용하는 반면, admin 페이지는 react를 사용하고, 공통 패키지에선 TypeScript만을 사용합니다.
이는 공통 lint의 rule을 더더욱 좁게 만들어, 각 패키지들에서 다시 setting 해야할 소요가 늘기 때문에 각 package에서 lint config를 하는 것과 다르지 않았습니다.
따라서, lint만을 위한 패키지를 따로 만들고 기술 스택에 따른 setting을 따로 해주어, 제공하는 것으로 해결했습니다.
(훗날 lint 셋팅을 open source로 공개하기 위함도 있습니다.)
도입 후 장점들!
(모두 한 곳으로 모여라!)
모노레포로 FE 소스들을 한 곳에서 관리함으로서 얻는 장점들은 당시 겪고 있던 문제들을 해결해주는데 적합했습니다.
- 동시 다발적인 코드의 변경 사항을 한 눈에 쉽게 파악할 수 있게 됐습니다.
repo가 나누어져 있을 때는, 각 repo마다 변경된 사항을 확인해야 했었습니다.
하지만, 어떤 feature는 해당 프로젝트 코드 뿐만아니라 다른 프로젝트의 코드 또한, 수정해야할 경우가 있습니다.
repo가 여러개 일 경우엔, 코드 변경이 산발적으로 흩어져 있어 이를 맥락적으로 파악하지 못했는데,
repo가 하나이므로, 맥락에 따른 코드 변경이 한 눈에 들어와 코드 변경을 훨씬 쉽게 파악할 수 있게 되었습니다.
- config와 lint가 통일됐습니다.
이렇게 됨으로서 코드를 일정하게 관리할 수 있게 됐습니다.
또한, 새롭게 프로젝트에 투입된 인원의 개발 환경 셋팅도 간편하게 할 수 있게 되었습니다.
- 하나의 work space에서 여러 프로젝트를 동시에 효율적으로 작업할 수 있게 되었습니다.
패키지 또한, 새로운 프로젝트로 별개의 repo를 갖습니다.
그렇게 패키지를 위한 work space를 열고 테스트코드에만 의존해 개발을 하고 패키지 repo에 코드를 올리고 PR과 코드 리뷰 등의 코드를 관리합니다.
그렇게 deploy를 명령하고 npm deploy가 완성되는 순간, 비로소 애플리케이션 프로젝트들에 적용할 수 있습니다.
하지만, 모노레포를 적용하고부터는 여러 프로젝트를 넘나들며 필요한 작업하지 않아도 됩니다.
공통 패키지는 yarn workspace를 통해 local로 연결돼 있으며, 개발한 로직을 애플리케이션의 dev 환경에서 직접 실시간으로 확인할 수 있습니다.
또한, 패키지 develop이 끝나고 “lerna publish” 커맨드로 npm publish와 함께 패키지를 의존하는 버전의 갱신까지 알아서 해줍니다!
앞으로의 develop 계획과 과제
뿌듯하게도 모노레포를 마무리했지만, 아직 해야할 과제가 많이 남아있습니다! 😁
yarn berry (3.x)
아직 이루지 못한 숙원인 yarn berry 업그레이드가 있습니다!
yarn berry의 Plug’n’Play mode를 통한 zero install로 빌드 속도 개선과 작업 환경 개선을 기대하고 있습니다.
npm package -> local dependency, lerna 제거
현재 애플리케이션이 아닌 패키지들은 모두 npm private으로 publish되어 있습니다.
현재 CI 빌드 관정에서 모노레포 환경을 통째로 docker container에 올리지 않고 단일 package만 docker container에 올려 빌드하고 있기 때문입니다.
하지만, 모노레포 프로젝트 이외에선 패키지들을 사용할 일이 없기 때문에, npm으로의 publish는 불필요하고 local에서 서로서로 dependency를 맺어 사용하는 것이 좋을 것 같다는 생각입니다.
이와 동시에 publish가 필요 없어진 모노레포에 lerna를 제거하려고 합니다.
이를 위해, build 프로세스부터 하나씩 개선해 나가야 할 것 같습니다.
마무리
현재 모노레포 도입을 성공적으로 마치고, 모노레포 프로젝트는 TBD 전략을 통해 브랜치와 코드를 관리하고 있습니다.
(TBD 내용은 다음 포스팅에서 다뤄보도록 하겠습니다! 🙌)
완벽하진 않지만 성공적으로 마친 것 같습니다!
개인이 개발을 잘하는 것도 중요하지만, 개발을 더 잘하게 하기 위한 환경을 개선하는 것도 중요하다고 생각합니다!
그런 점에서 모노레포는 개발 환경을 크게 개선해주었고 스스로도 개발을 하면서 개발 효율이 늘었다는 것을 느끼고 있어 뿌듯합니다! 😁
앞으로도 오늘 1을 할 수 있었다면 내일을 그 2배를 할 수 있도록 꾸준히 노력하려고 합니다! 🙌
refs
모던 프론트엔드 프로젝트 구성 기법 - 모노레포 도구 편 by Naver D2