스타트업 개발자의 모노레포 도입기 3편
Xperience

스타트업 개발자의 모노레포 도입기 3편

모노레포 구축 과정과 도입 후기

#Monorepo#PNPM
8
1,575

시작하기 전에

이 글은 모노레포 도입기 시리즈의 3편입니다.
이전 포스팅에서는 모노레포 도입 배경, 도구 선택 과정과 그 이유를 다뤘습니다.
👉 1편: 레거시 전쟁의 끝을 내러 왔다: 모노레포 도입 배경과 장단점
👉 2편: 모노레포 도구 선택: PNPM과 Turborepo, 너로 정했다! ⚡️

이번에는 실제 모노레포 구축 과정과 도입 후의 변화, 그리고 아쉬웠던 점들을 다루어 볼 예정입니다.

모노레포 구축 과정

1. 최소한의 서비스 환경 통일

모노레포를 구축하기 전 회사 서비스 레포지토리 환경은 멀티레포 구조였습니다.

프로젝트 별 다양한 환경과 설정이 존재하는 멀티레포 아키텍처
프로젝트 별 다양한 환경과 설정이 존재하는 멀티레포 아키텍처

멀티레포각 레포지토리에 서비스가 독립적으로 운영되는 형태입니다. 각 서비스는 자체적인 노드 환경, 의존성 버전, 코드 컨벤션, 린팅 등 다양한 설정을 개별적으로 관리합니다.

이러한 독립성은 각 프로젝트가 필요한 환경과 설정을 자유롭게 선택할 수 있고, 다른 프로젝트를 신경 쓰지 않아도 된다는 장점이 있습니다. 특히 조직 내 팀 간 개성이 뚜렷할 경우 이러한 자율성은 큰 장점이 될 수 있습니다.

하지만 장기간 방치된 코드 베이스와 시대별로 다른 관리자들이 만든 프로젝트들이 쌓여 만들어진 멀티레포 환경의 독립성은 이를 모두 유지 보수해야 하는 개발자 입장에서는 오히려 큰 부담이 됩니다.

프로젝트를 오갈 때마다 NVM으로 노드 버전을 변경하고, 낡은 의존성으로 인해 구식 문법을 사용해야 하며, 서로 다른 스타일 도구와 컨벤션을 적용해야 합니다. 이러한 차이를 유지할 특별한 이유가 없다면, 통일하는 것이 더 효율적일 것입니다.

따라서 모노레포 구축의 첫 단계는 각 서비스 레포지토리의 최소한의 환경과 의존성 버전, 컨벤션을 통일하는 것이었습니다. (노드 환경과 의존성 업그레이드는 서비스 전반에 큰 영향을 미칠 수 있는 중요한 작업이므로, 테스트 코드를 통한 검증과 충분한 테스트 기간이 필요합니다)

이러한 작업은 모노레포 마이그레이션 이전에 각 서비스에서 별도의 브랜치를 만들어 여유롭게 진행하는 것이 모노레포 마이그레이션 이후에 진행하는 것보다 형상 관리나 충돌 방지에 더 용이합니다.

2. 모노레포 구조 설정

모노레포에는 프로젝트가 존재하는 apps 디렉토리와 공통 컨벤션 및 라이브러리를 모아두는 packages 디렉토리가 있습니다.

apps와 package
apps와 package

3. 서비스 코드 & 깃 히스토리 마이그레이션

멀티레포 내 서비스를 모노레포로 마이그레이션할 때 가장 중요한 것은 깃 히스토리를 보존하는 것입니다.

깃 히스토리를 보존함으로써 코드의 변경 이력을 추적하고, 추후에 버그가 발생했을 때 관련 개발자를 찾아 문의하거나, 롤백 가능성을 확보할 수 있습니다.

저는 git mvgit merge --allow-unrelated-histories 명령어를 활용해 마이그레이션을 진행했습니다.

마이그레이션 방법에 대한 자세한 설명은 아래 게시글을 참고해주세요 !

4. 공통 의존성, 노드 중앙 집중식 관리

apps 내 각 프로젝트에는 React와 같은 공통 의존성이 존재할 수 있습니다. 통일된 노드 환경에 맞는 의존성 버전을 지정하고, 이를 모든 프로젝트에서 공유한다면, 의존성 버전 일관성이 보장되고, 중앙 집중식 버전 관리로 인해 유지보수가 편리해질 것입니다.

이러한 공통 의존성의 버전은 모노레포 루트의 package.json 에 명시하여 관리할 수 있습니다. 그 외의 의존성은 각 프로젝트 내의 package.json에 별도로 명시하여 사용합니다.

Node 환경과 의존성 버전 통일 및 중앙 집중식 버전 관리
Node 환경과 의존성 버전 통일 및 중앙 집중식 버전 관리

모노레포 루트의 package.json에 공통으로 사용할 노드 환경 버전과 패키지 매니저 버전을 명시합니다.

{ "engines": { "node": ">=20", "pnpm": ">=9" } }

이제 앞으로 생성되는 모든 package, apps 내의 프로젝트가 명시된 버전을 사용하도록 하기 위해 모노레포 루트의 .npmrc에 엔진 엄격 구성 플래그인 engine-strict=true 옵션을 true로 설정합니다.

engine-strict=true

5. 컨벤션

다음으로 기본적인 린팅, 포맷팅 컨벤션을 통일합니다. 여러분이 만약 여러 프로젝트를 왕래하여 작업하거나, 코드리뷰를 진행한다면 프로젝트 간 기본적인 컨벤션을 통일하는 것이 작업에 용이할 것입니다.

packages 폴더에는 apps 내의 프로젝트에서 공통으로 사용되는 컨벤션을 폴더별로 구분해 두고, 이를 공유하여 사용합니다. 필요한 경우 각 프로젝트에서 이를 상속받아 추가적인 설정을 적용할 수 있습니다.

아래 예시에선 모든 프로젝트에 적용되는 Prettier 설정을 packages/config-prettier 폴더에 두고, 이를 공유하여 사용하는 경우를 보여줍니다.

packages/config-prettier 내 공유 프리티어 설정 적용
packages/config-prettier 내 공유 프리티어 설정 적용

apps의 프로젝트에선 다음과 같이 공유 설정을 참조하도록 합니다.

{ ... "devDependencies": { "@repo/prettier-config": "workspace:*", "@repo/tailwind-config": "workspace:*", "@repo/typescript-config": "workspace:*", ... }, "eslintConfig": { "extends": ["@repo/config-eslint/react"] }, }

6. 공통 UI 컴포넌트 프로젝트

지난 게시글에서, 스타일 방식을 통일한 뒤, 공통 UI 컴포넌트를 분리하는 작업을 진행했습니다.

하지만 공통 UI 컴포넌트의 코드를 각 프로젝트에서 직접 복사 붙여 넣기 하여 사용하는 방식 때문에, UI 컴포넌트의 변경 사항을 모든 프로젝트에서 동시에 반영하기가 어려웠습니다.

이를 모노레포 내의 별도 프로젝트인 apps/design-system으로 분리하여 UI 컴포넌트를 별도로 관리하여 프로젝트에서 빌드된 UI 컴포넌트를 여러 프로젝트에서 공유할 수 있도록 하고자 합니다.

packages/ui와 같이 패키지 디렉토리 내부에 위치할 수도 있지만, 저는 사내 디자이너들이 UI 컴포넌트 테스트를 위해 접근할 수 있는 사내 프로젝트라고 생각했기 때문에 apps 디렉토리 내에 위치하도록 했습니다.

apps/design-system 내부에는 각 UI 컴포넌트별(ex. Button, Input, Modal)로 폴더를 구분하여 문서화해 두고, Storybook을 통해 테스트 환경을 구성합니다.

컴포넌트 디렉토리 내 UI 컴포넌트, Storybook 파일
컴포넌트 디렉토리 내 UI 컴포넌트, Storybook 파일

이후 index.ts 파일에서 각 컴포넌트를 불러온 뒤 tsup을 통해 빌드합니다.

index.ts에서 모든 컴포넌트 래핑
index.ts에서 모든 컴포넌트 래핑
index.ts 빌드
index.ts 빌드

빌드된 결과물을 다른 프로젝트에서 디폴트로 import할 수 있도록 package.json을 수정합니다.

... "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.js" }, "./*": { "types": "./dist/*/index.d.ts", "import": "./dist/*/index.mjs", "require": "./dist/*/index.js" } }, "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", ...

이후 각 서비스 프로젝트의 package.json에서 UI 프로젝트를 workspace:*로 추가하면, 빌드된 UI 프로젝트의 컴포넌트를 사용할 수 있습니다.

의존성에 UI 프로젝트 추가
{ "dependencies": { "design-system": "workspace:*" } }
UI 프로젝트 내의 Button 컴포넌트 사용
import { Button } from "design-system"; export default function Page() { return ( <main className="flex flex-col items-center justify-between min-h-screen p-24"> <Button variant="danger" size="lg" label="Hello, World!" /> </main> ); }

이제 모든 프로젝트에서 UI 컴포넌트 소스 코드를 관리하지 않아도, 모노레포 내의 UI 프로젝트에서 작업한 결과물을 빌드만 하면 여러 프로젝트에서 동시에 변경된 결과물을 사용할 수 있게 되었습니다.

BRAVO !

각기 다른 노드 환경과 의존성, 스타일 도구는 그 자체로 문제가 되지 않습니다. 하지만 소규모의 개발자가 많은 레거시 프로젝트를 유지보수 하는 상황에서는 분명 걸림돌이 됩니다. 따라서 이들을 통일하기 위해 각 프로젝트의 기존 로직과 요구 사항, 정책 등을 파악하고 이해하기 위해 많은 노력을 기울였습니다. 그 첫걸음인 모든 스타일링 도구를 통일하는 것을 시작으로, 단계적으로 하나씩 문제를 해결해 나가다 보니 결과적으로 이렇게 거대한 하나의 모노레포를 구축하게 되었습니다.

이제 모노레포 도입을 통해 어떤 긍정적인 변화가 생겼는지 소개해 보겠습니다.

모노레포 도입 결과

최종 모노레포 아키텍처
최종 모노레포 아키텍처

최종적으로 모노레포를 도입한 뒤의 전반적인 레포지토리 구조는 위와 같습니다.

  • 모든 프로젝트는 같은 버전의 노드 환경과 패키지 매니저를 공유합니다.
  • 공통 의존성은 모노레포 루트에서 관리하며, 각 프로젝트는 모노레포 루트에서 공통 의존성을 상속받아 사용합니다.
  • 기본적인 코드 컨벤션은 패키지 내에 작성해 둔 뒤 이를 상속받아 사용합니다.
  • Design System에서 작성된 UI 컴포넌트는 빌드되어 각 서비스에서 손쉽게 import하여 사용할 수 있습니다.

모노레포 도입 후 변화

1. 일관된 개발자 경험

모노레포 도입 후 가장 크게 바뀐 변화는 단연코 일관된 개발자 경험 제공으로 인한 신속한 컨텍스트 스위칭이었습니다.

더 이상 프로젝트를 오갈 때마다 NVM으로 Node 버전을 스위칭할 필요가 없어졌고, React 같은 메이저 라이브러리부터 작은 유틸리티까지 공통으로 사용하는 라이브러리 버전이 통일되어 프로젝트 간 일관된 코드 작성이 가능해졌습니다. 프로젝트마다 제각각이던 린팅, 포맷팅 규칙도 하나로 통일되어 저장 버튼을 누를 때마다 당황하는 일이 줄어들었습니다. 불필요한 시간낭비가 없어졌죠 !

그렇다고 일이 줄어드는 건 아니에요.
그렇다고 일이 줄어드는 건 아니에요.

2. 향상된 개발 생산성

개발 생산성 측면에서도 많은 이점이 있었습니다. 특히 주요 의존성, 컨벤션, 스타일 설정 등 대부분의 설정이 패키지 형태로 세팅되어 있어 새로운 프로젝트를 손쉽게 구축할 수 있게 되었습니다. 실제로 사내 키패드 라이브러리 도입을 돕기 위한 샘플 페이지를 구축하는데 단 몇 분밖에 걸리지 않았죠.

파편화되어 있던 컨벤션과 UI 코드도 이제는 공통 프로젝트에서 관리되어 일관성 유지와 유지보수가 한결 수월해졌습니다. UI 컴포넌트를 변경할 때도 로컬 빌드만으로 모든 프로젝트에 즉시 반영할 수 있게 되었고, 공통 컨벤션을 변경할 때도 중앙 집중식 관리 덕분에 작업 공수가 크게 줄었습니다.

3. 프로젝트 리소스 관리

프로젝트 리소스 관리 측면에서도 큰 개선이 있었습니다. 모든 프로젝트에서 사용하는 라이브러리는 이제 한 곳에서 버전을 관리하여 일관성을 유지할 수 있게 되었고, 동일한 버전의 패키지를 재사용함으로써 불필요한 중복 설치도 방지할 수 있게 되었습니다. 특히 UI 컴포넌트는 단 하나의 프로젝트에서만 관리되기 때문에 관심이 분산되지 않고 효율적인 코드 관리가 가능해졌습니다.

많은 프로젝트를 적은 인원이 관리해야 하는 상황에서, 모노레포 도입은 업무 효율을 크게 개선하는 터닝포인트가 되었습니다. 이 변화가 단순히 개발 환경의 개선을 넘어, 미래의 팀 생산성과 코드 품질을 전반적으로 향상시키는 결과를 가져올 것이라 믿습니다!

아쉬운 점

모노레포 도입으로 많은 이점을 얻었지만, 동시에 몇 가지 아쉬운 점들도 있었습니다.

1. 복잡해진 버전 관리

사내 제품들은 라이브러리 형태로 제공되는 것이 많았고, 각 서비스의 레포지토리에서 라이브러리 릴리즈 버전 관리를 진행했습니다. 하지만 모노레포에서는 같은 방식으로 릴리즈 버전 관리를 진행할 수 없게 되었습니다. 현재 깃 태그를 통해 각 제품의 릴리즈 시점을 저장해두고 있지만, 좀 더 나은 방법이 있다면 이를 적용해보고 소개해보고자 합니다.

2. Git 저장소 크기 증가

당연하게도, 여러 프로젝트가 하나의 저장소에 통합되면서 Git 저장소의 크기가 크게 증가했습니다. 이는 초기 클론 시간이 오래걸리는 결과를 가져왔습니다.

3. 엄격한 협업 컨벤션의 강제성

모노레포에는 여러 프로젝트의 커밋 히스토리가 하나의 타임라인에 혼재되기 때문에, 커밋, PR 등에 엄격한 컨벤션을 적용해야합니다. 그렇지 않을 경우 특정 변경 사항을 추적하기가 매우 어려워질 수 있습니다.

4. CI/CD 파이프라인 구성의 복잡성

각 프로젝트별로 다른 배포 환경과 설정이 필요한 경우, 이를 하나의 파이프라인에서 관리하는 것이 복잡하고 가독성이 떨어질 수 있습니다.

5. 대규모 조직에서의 접근 권한 세분화 어려움

직접 살에 와닿은 단점은 아니었지만, 만약 대규모 조직이라면 접근 권한 세분화의 어려움으로 인한 보안관리에 대해 충분히 고려가 필요할 것이라 생각했습니다.

다행히도 프론트엔드 개발 생태계가 계속해서 발전하면서, 이러한 아쉬운 점들을 해결할 수 있는 다양한 도구들이 속속 등장하고 있습니다! 새로운 도구들과 해결 방안들에 대해서도 앞으로 하나씩 소개해드리도록 하겠습니다.

마치며

이번 글에서는 모노레포 구축 과정과 도입 후의 변화, 그리고 아쉬운 점들을 상세히 다뤄보았습니다. 모노레포라는 새로운 아키텍처를 도입하는 과정은 분명 도전적이었지만, 그만큼 값진 경험이었습니다.

특히 개발자 경험 개선이라는 측면에서 큰 성과를 거두었다고 생각합니다. 프로젝트 간 일관된 개발 환경, 통일된 스타일, 중앙화된 의존성 관리, 표준화된 컨벤션은 개발 생산성을 매우 향상시켰습니다. 프로젝트가 한둘도 아닌데다, 하나에 겨우 익숙해지면 다른 프로젝트로 넘어가 또다시 적응해야 했던 그 불편함과는 이제 작별인사를 했습니다.

…저번에 이 린트 규칙 무시하지 않았나?
…저번에 이 린트 규칙 무시하지 않았나?

물론 여전히 해결해야 할 과제들이 남아있습니다. 버전 관리의 복잡성, Git 저장소 크기 증가, CI/CD 파이프라인 구성 등 아직 개선의 여지가 있는 부분들이 있죠. 실제로 이미 여러 개선 사례를 찾아볼 수 있었습니다. 앞으로는 이러한 사례들을 소개해드리고, 시행착오와 해결 방안들을 계속해서 공유하고자 합니다.

이 시리즈를 통해 모노레포 도입을 고민하시는 분들께 조금이나마 도움이 되었기를 바랍니다. 앞으로도 도움이 될 만한 제 경험들을 공유하고, 더 나은 개발 문화를 만들어가는데 기여하고 싶습니다.

궁금하시거나 더 자세히 알고 싶으신 부분이 있다면 언제든 댓글로 남겨주세요! 함께 고민하고 발전해나가면 좋겠습니다!