엘리스에서 인턴 생활 중, 모노레포를 건의하고 이를 실제로 적용한 경험을 이야기하고자 합니다.
1. 모노레포로 마이그레이션을 결정하기까지
기존에 저희 엘리스에는 7갸의 랜딩 페이지가 각 멀티레포로 구성되어 있었습니다. 하루는, 7개의 랜딩 페이지의 헤더 디자인이 수정하는 업무를 배정받았습니다. 당시, 7개의 레포를 하나씩 들어가 수정하고, MR을 올리고, 메인테이너인 사수님께 어푸르브를 받고, 그제서야 머지를 했습니다. 이 과정에서 7개의 레포를 왔다갔다 하며 작업을 해야 했고, 이는 상당히 번거로운 작업이었습니다.
이에, 제가 최근에 사이드 프로젝트에서 도입한 '모노레포'를 생각해냈습니다. 이를 먼저 사수님과 이야기해본 후, 사수님은 이를 긍정적으로 받아들이셨습니다. 그리고, 팀장님과 팀원들에게 동의를 구해야했습니다. 제안을 하기에 앞서, 모노레포로 마이그레이션 하면 좋은 이유들에 대해 생각해보고, 문서화를 했습니다.
모노레포 관리 도구를 결정하는 데 있어서 가장 중요하게 고려한 요소는 star수와 최근까지 커밋이 올라오는가였습니다. star수가 많을 수록, 그리고 최근까지 커밋이 올라오는 레포일 수록 안정적이라고 판단했습니다. 최근까지도 유지/보수를 하고 있고, 업데이트를 하고 있구나 라는 것을 알 수 있기 때문입니다.
위 요소를 고려했을 때 NX와 Turbo Repo를 후보로 남겨두었습니다. NX는 22.6k의 star, 그리고 Turbo Repo는 25.4k의 star수를 가지고 있습니다. 두 도구의 특징을 살펴보면 NX는 Google에서 개발을 하였고, Turbo Repo는 Vercel에서 개발을 하였습니다. NX는 Node.js 기반으로 작성되어 있고, Turbo Repo는 Rust 기반으로 작성되어 있습니다. 그리고, NX는 VS Code 확장 프로그램을 지원하고, Turbo Repo는 지원하지 않습니다.
최종적으로, 비교적 낮은 학습 곡선으로 팀 적응이 빠를 것이라는 점, 그리고 현재 프로젝트들이 Next.js기반이므로 Vercel 생태계와의 완벽하게 호환된다는 점에서 Turbo Repo를 선택했습니다.
2. 본격적인 마이그레이션
1. TurboRepo 설정
TurboRepo 설치
TurboRepo 설정 파일(turbo.json) 생성
패키지의 scripts에 turbo 명령어 추가
2. 폴더 구조
가장 루트에는 공통적으로 관리할 .gitlab, .vercel, .vscode, .turbo 등의 폴더와 파일이 위치합니다. 그리고, 각 서비스는 apps 폴더에 위치하고, 공통으로 관리할 패키지는 packages 폴더에 위치합니다. packages폴더에는 공통으로 관리하는 컴포넌트를 담은 components, 훅을 담은 hooks, 유틸 함수를 담을 utils폴더가 존재합니다.
3. Typescript, ESLint, Prettier 설정
1) Typescript
Typescript는 루트의 tsconfig.json 파일에 공통으로 적용할 옵션들을 추가하고, 각 패키지에서 이를 확장하여 사용했습니다. 각 패키지에서 사용하고 있던 typescript 버전이 달라, 이를 각 패키지에서 설치하여 관리했습니다.
루트: 공통되는 Typescript 설정을 관리합니다.
각 패키지: 루트의 tsconfig.json을 확장하고, 각 패키지에 알맞게 설정을 추가합니다.
2) ESLint
ESLint는 가장 가까운 .eslintrc.* 설정 파일을 찾아서 사용합니다. 각 패키지 별로 설정된 ESLint 설정이 달랐기에, 루트에서 관리하고 각 패키지 별로 이를 확장하여 사용했습니다.
루트
각 패키지
3) Prettier
prettier는 가장 가까운 .prettierrc 파일을 찾아서 사용합니다. 만약 패키지마다 prettier 설정을 다르게 하고 싶다면, 각 패키지에 .prettierrc 파일을 추가하면 됩니다. 하지만, 모두 공통적으로 적용하고 싶다면, 루트에만 .prettierrc 파일을 두면 됩니다. 프로젝트 전체에서 동일한 코드 스타일을 유지하는 것이 중요하기 때문에 루트 .prettierrc 파일 하나만을 사용하는 것을 추천합니다.
4. CI/CD 설정
GitLab CI는 .gitlab-ci.yml 파일을 참조하여 실행합니다. 이 파일은 루트에 위치해야 하고, 기존에 각 레포 별로 상이하던 CI와 CD 설정을 하나의 파일로 통일하여 관리해야 했습니다.
1) CI 설정
CI는 lint 검사와 build를 수행합니다. lint와 build 과정을 모듈화하고, 이를 .gitlab-ci.yml에서 가져와 사용하도록 했습니다.
changes 옵션을 활용해서, 변경이 발생한 패키지만 lint 및 build 검사를 수행하도록 했습니다.
2) CD 설정
저희는 Vercel CLI를 이용하여 배포를 하고 있었습니다. 태그를 추가하면 배포를 실행하도록 하였습니다.
5. TurboRepo의 활용
병렬 실행과 캐싱
TurboRepo는 병렬 실행과 캐싱을 통해 빌드 시간을 크게 단축할 수 있습니다. 동일한 작업이 반복될 때, 캐시를 사용하여 중복된 작업을 피할 수 있습니다.
의존성 기반 실행
TurboRepo는 의존성 그래프를 사용하여 필요한 작업만을 효율적으로 실행합니다. 예를 들어, 특정 패키지에 변경 사항이 발생했을 때, 해당 패키지와 관련된 작업만을 실행합니다.
CI/CD 파이프라인 최적화
CI/CD 파이프라인에서 TurboRepo를 사용하면 변경된 패키지에 대한 작업만을 실행함으로써 시간을 절약할 수 있습니다. GitLab CI/CD 설정에 TurboRepo를 적용하여, 각 패키지의 변경 사항에 따라 빌드 및 배포 작업을 수행했습니다.
효과
1. 공통 컴포넌트 사용
각 레포 별로 공통 컴포넌트를 사용할 수 있습니다. 기존에는 각 서비스에서 공통 컴포넌트를 사용하기 위해 해당 컴포넌트를 복사하여 사용해야 했습니다. 하지만, 모노레포로 마이그레이션하면서 공통 컴포넌트를 package/ui 폴더에 넣어두고 각 서비스에서 이를 가져다가 사용할 수 있게 되었습니다.
2. 기능 추가 용이성
마이그레이션 이후, 실제로 모든 서비스에 이벤트 배너를 추가해야 하는 업무를 배정받았습니다. 공통 컴포넌트를 만들어 각 서비스에 추가한 후 한 번의 MR과 머지로 7개의 서비스에 반영이 가능해졌습니다.
단점
1. 초기 설정의 복잡함
이제는 설정을 어느 정도 완료하여 안정적인 상태에 접어든 상태지만, 마이그레이션 과정에서 각각의 레포지토리에서 독립적으로 관리되던 설정들을 통합하고 공통된 설정 파일들을 구성하는 과정에서 많은 에러를 마주했습니다. 이 과정에서 팀원 두 명이 2주라는 적지 않은 시간을 소모해야 했습니다.
2. 더 많은 신경을 써야 한다.
각 패키지별로 레포지토리를 관리할 때는 다른 프로젝트에 대해서는 신경 쓸 필요가 없었지만, 이제 하나의 변경 사항이 모든 프로젝트에 영향을 미칠 수 있습니다. 특히 공용 패키지를 수정할 때 모든 패키지에 영향을 줄 수 있기 때문에, 변경 사항을 주의 깊게 관리해야 합니다.
트러블슈팅
1. couldn’t find package @elice/mui-elements required by ~ on the ‘npm’ registry
@elice/mui-elements는 npm이 아니라, elice gitlab 레포에 존재합니다. 따라서 .npmrc 파일을 추가해줘야 합니다. .npmrc에는 해당 레포의 위치와 토큰값을 설정합니다.
2. @elice/prettier-config@1.220803.0 The engine “node” is incompatible with this module. Expected version “~16.14”. Got “20.5.1”
기존에 각 패키지 별로 사용하고 있던 prettier config의 버전이 달라, 각 버전 별로 요구하는 Node 버전이 달라 발생한 에러입니다. 이는 prettier와 prettier-config를 루트에서 관리하면서 해결했습니다.
3. Parsing error: ESLint was configured to run on <tsconfigRootDir>/apps/elice-project-landing/babel.config.js using parserOptions.project: /../elice-landing-mono/apps/elice project-landing/tsconfig.json However, that TSConfig does not include this file.
ESLint는 현재 파일(babel.config)을 구성 안에 넣었는데, tsconfig에서는 해당 파일을 포함하지 않고 있습니다.
원인: tsconfig에서 현재 파일(babel.config.js)를 포함하지 않기에 발생한 에러입니다.
해결: tsconfig.json > include 배열에 **/*.js를 추가하여 모든 파일을 적용하도록 합니다.
결론: 위의 에러의 원인은 eslint에서 현재 parserOptions:{project:true}를 사용하고 있기 때문입니다. 이 옵션은 eslint가 가장 가까운 tsconfig.json 파일을 찾아간다는 의미입니다. 즉, 해당 tsconfig.json에서 eslint를 적용할 모든 파일들을 추가해야 합니다. eslint를 적용하지 않으려면, .eslintignore 파일에 추가해야 합니다.
마무리하며
모노레포로의 마이그레이션은 두 명의 개발자가 꼬박 2주 정도 걸리는 상당히 큰 프로젝트였습니다. 단순한 설정 변경 이상의 도전 과제들을 안겨주었습니다. 초기 설정의 복잡함, 빌드 및 테스트 시간의 증가, 패키지 버전 관리의 어려움 등 다양한 문제를 직면했지만, 이 과정에서 많은 것을 배울 수 있었던 거 같습니다.
멀티레포의 불편함을 인식하고 모노레포로의 전환을 팀장에게 제안하여 실제 적용까지 이끌어낸 것은 매우 보람찬 경험이었습니다. 이 변화로 팀원들의 생산성이 크게 향상되었고, 개발 환경이 더욱 편리해질 것이라는 기대에 큰 뿌듯함을 느낄 수 있었습니다.
앞으로도 팀원들의 생산성을 높여주는, 그리고 도전적인 프로젝트를 진행하며 가파르게 성장하는 개발자가 되고 싶다는 생각을 했습니다.