모노레포 구축하기

Jordan Choi·2024년 2월 10일
0
post-thumbnail

모노레포 초기 구축 배경

신규 기획 개발을 앞둔 2022년 12월, 프론트엔드 팀에선 다음의 문제를 직면했습니다:

  • 개발 인력에 비해 관리해야 할 프로젝트 개수가 많아 주로 개발하는 프로젝트 외 유지 보수가 어렵다.
  • 레포를 생성할 때마다 DevOps를 새로 구성해야 한다. 프로젝트 생성 표준이 없어 초기 환경 구축에 많은 비용이 소모된다.
  • 프로젝트 별 개발 환경이 상이해 개발 경험의 차이가 발생한다.
  • 각 프로젝트 간 중복된 기능 구현이 잦다.
    • 매번 새로 개발하는 경우 코드 퀄리티의 장기적인 향상이 어렵고, 불필요하게 개발 리소스를 낭비하게 된다.
    • 기존 코드를 참고할 경우 잠재적 버그가 전이될 가능성이 있다.
  • 담당 프로젝트 외의 프로젝트는 접근성이 떨어진다.

모노레포는 어떤 문제를 해결하는가?

  • 공통 로직을 한 군데서 관리함으로써 개발 리소스 낭비를 줄인다.
    • 개발 리소스는 수정사항을 여러 군데에 적용하는 리소스, 패키지 버저닝 및 릴리즈 비용을 포함한다.
  • 프로젝트 생성 표준을 바탕으로 generators를 설정하여 통일된 개발 환경을 적은 비용으로 구축한다.
  • 공통된 프로젝트 환경은 각 프로젝트 별 커맨드를 확인하는 등의 정신적인 오버헤드를 감소시킨다 (컨텍스트 스위칭 비용 감소).
  • 코드 변경점의 영향 범위를 모노레포 도구를 이용해 빌드 및 테스트하여 사이드이펙트를 방지한다.
  • 일관된 개발 환경과 코드 컨벤션으로 개발자 간 협업 능률을 올린다.
    • 배포, 브랜치 전략 등의 표준화로 각 프로젝트 간 컨텍스트 스위칭 비용과 러닝 커브를 낮춘다.

모노레포 도구 선택: Nx

모노레포 요구사항 리스트업하기

모노레포 도구 선택 전 필요한 요구사항을 먼저 리스트업했습니다.

  • js, ts 친화적인 모노레포 도구여야 한다.
  • 초기 환경구축 과정이 쉬워야 한다. 러닝커브가 낮을수록 좋다.
  • 빌드 속도는 빠를수록 좋다.
    • 각 패키지의 변경된 부분만 개별적으로 배포할 수 있어야 한다.

      Affected changes upon code update

      Affected changes upon code update

  • 각 패키지의 의존성 관리가 용이해야 한다
    • 각 패키지에서 의존하는 패키지 목록과 그 버전에 대해 한 눈에 파악할 수 있어야 한다.
    • 특정 패키지에 대해 모노레포 내 모든 패키지가 동일한 버전을 사용하는 경우, 버전을 한 번에 업데이트할 수 있어야 한다.
  • 모노레포 내 모든 패키지에 동일한 린트 규칙을 적용해야 한다.
  • 각 패키지마다 사용하는 프레임워크, 의존성, 배포 시기가 모두 다르므로, 패키지별 독립적인 배포가 보장되어야 한다.
    • 패키지별 버전 관리가 필요하다.
  • 모노레포 관리 비용이 적을수록 좋다.
    • 모노레포 관련 이슈 발생 시 빠른 대응 및 대응 리소스를 최소화하기 위해 안정적으로 운영 중이어야 하며, 커뮤니티 규모가 커야 한다.
      • 생태계가 크면 vscode extensions나 plugins 등 사용할 수 있는 도구가 많아지기 때문이다.
    • 문서화가 잘 되어있을수록 좋다.
  • 새 패키지 생성이 쉬워야 한다 (Code generation).

모노레포 도구 정하기

리스트업한 요구사항을 바탕으로 모노레포를 구현할 때 어떤 도구를 선택할 지 결정했습니다. 후보군은 Nx, Turborepo였습니다.

패키지 매니져 워크스페이스를 고려하지 않은 이유

모노레포 구축 당시 대다수의 프로젝트에서 yarn classic을 사용 중이이었고, yarn berry로의 이관 비용을 제외하기 위해 yarn classic workspaces에 대해서만 고려했습니다.
yarn workspaces만 사용해도 코드 공유 목적은 충분했으나, 다른 모노레포 도구를 사용할 경우 다음과 같은 모노레포 기능을 사용할 수 있기에 후보군에서 제외하였습니다 (참고: Monorepo features).

  • 로컬 캐시 / 리모트 캐시를 이용한 빌드, 린트, 테스트 시간 단축
  • 코드 변경사항에 영향 받는 패키지에 대해서만 커맨드 실행 가능

Nx를 모노레포 도구로 선택한 이유

1. 생태계가 크다.
생태계가 큰 nx는 turborepo와 달리 vscode extension( Nx Console)을 제공하고 있었으며, 플러그인 오픈소스가 많았습니다.

  • 각 플러그인에서는 여러 프레임워크에 대한 generators도 제공합니다 (e.g., @nx/next:generators).

또한 이슈 발생 시 참고할 자료가 많았기에 빠른 개발환경 구축에 안정감을 더해주었습니다.

2. 문서에 사용법이 자세히 기술되어 있다.
Nx는 turborepo보다 문서화가 잘 되어있었습니다. 공식문서에서 다양한 솔루션을 제공할 뿐 아니라, Nx Core 팀에서 운영하는 블로그에서 새로 배포한 버전에 대한 안내, 환경 구축 팁, 문서화 팁 등을 포스팅했습니다.

3. package-based 방식 뿐 아니라 Integrated 방식도 지원한다.
package-based repository는 모노레포 내 각 패키지마다 package.json을 관리하고, 서로를 package.json을 통해 의존하는 방식입니다. 패키지 매니져 워크스페이스 등 일반적인 모노레포에서 의존성을 관리하는 방식입니다.
Nx는 package-based 방식 뿐 아니라 Integrated 방식도 지원합니다. Integrated repository는 패키지간 package.json이 아닌 typescript imports를 통해 의존하고, root에 하나의 package.json만을 관리하며 single version policy를 지킵니다.
Integrated 방식의 경우 기존 패키지를 모노레포에 추가할 때 빌드 도구 등을 모노레포에서 사용하는 방식으로 마이그레이션해야 하는 단점이 있지만, 모노레포의 규모가 커져도 의존성 관리가 용이한 장점이 있습니다.
당시 저희 팀은 각 프로젝트의 기술 스택이 크게 다르지 않았고, 프로젝트 개수가 많았기에 추후 의존성 관리가 용이한 integrated 방식이 적합하다고 판단했습니다. 또한 Integrated repository로 구성할 경우 Nx를 좀 더 프레임워크에 가깝게 사용할 수 있기에 저희가 신경 쓸 수 없는 부분을 Nx가 대신 해주길 바랐습니다.

4. 변경사항에 영향받는 범위를 affected 커맨드로 확인 및 제어할 수 있다.
모노레포는 많은 프로젝트를 하나의 디렉토리에서 관리하고 있기 때문에 규모가 커질수록 빌드 속도가 비례하여 증가합니다.
Nx는 이에 대한 솔루션으로 affected 커맨드를 제공합니다. 예를 들어, nx affected -t build 커맨드 실행 시 현재 코드 변경사항에 영향받는 워크스페이스 내 패키지를 리스트업하고, 해당 패키지 리스트에 대해서만 요청 작업(build)을 실행합니다.

모노레포 구축하기

1. 구조 잡기

Nx에서 권장하는 파일 구조로 구성하였습니다.

  • /libs/하위에는 아직 모노레포로 이관하지 않은 다른 앱에서도 공통으로 사용할 라이브러리 패키지(publishable)와 모노레포 하위에서만 사용하는 내부 패키지(buildable)를 관리하였습니다.
  • /apps/ 하위에는 서비스로 운영 중인 앱 패키지를 관리하였습니다.
rs-frontend/
└── libs/ # buildable한 라이브러리와 publishable한 라이브러리 존재
	└── publishable-library-package-1
	└── buildable-library-package-1
	└── ...
└── apps/
	└── app-package-1
	└── ...
└── nx.json
└── package.json
└── tsconfig.base.json

2. 개발 환경 구성하기

모노레포에서의 DX를 향상하기 위한 기본적인 개발 환경을 구성하였습니다.

  • 사내 린트 패키지를 사용하였지만 각 프로젝트 별로 린트 규칙을 덧붙이는 경우가 많아 프로젝트 별로 린트 규칙이 약간씩 상이하였습니다. 일관적인 개발 경험을 제공하고 초기 프로젝트 구축 비용을 줄이기 위해 eslint 공통 설정을 루트에 두고 각 패키지에서 이를 override하여 사용하도록 하였습니다.
    • 사내 eslint 패키지의 경우 다른 팀도 공통으로 사용하고 있었기에 저희 팀에서만 사용할 기본 공통 규칙을 지정하였습니다 (e.g., import/order, unused-imports)
  • 사내 커밋 메세지 규칙에 맞게 커밋 시 린트하는 과정과 푸쉬 전 빌드하는 과정을 husky, lint-staged, commitlint를 이용해 자동화하였습니다.
  • tsconfig도 eslint와 마찬가지로 루트에 기본 설정(tsconfig.base.json)을 두고 각 프로젝트 루트에서 이를 상속한 tsconfig를 별도로 관리하도록 하였습니다.
  • 사내 프론트엔드는 모두 개발 에디터로 vscode를 사용하며, Nx에서 제공하는 vscode extension과 같이 DX 향상을 위해 쓰면 좋은 vscode extensions를 설치 권장하기 위해 workspace recommended extensions를 설정하였습니다.

3. git branch 전략 세우기

모노레포를 도입하며 Trunk-based development를 도입하였습니다. Trunk-based development는 trunk 브랜치(main) 하나만을 두고 지속적으로 작은 단위의 코드를 trunk에 병합해나가는 버전 관리 전략입니다. 모노레포의 경우 코드 베이스 크기가 멀티 레포에 비해 크며, 병합 충돌이 일어날 경우 사이드이펙트가 커지기에 작은 단위의 코드를 보다 자주 병합할 수 있는 trunk-based development를 채택했습니다.

4. 배포 파이프라인 구축하기

각 패키지 별로 독립적인 배포를 보장하기 위해 CI 액션은 공통으로, 배포 액션은 패키지별로 설정하였습니다. 사내에서 자동 배포하는 프로젝트는 없었기 때문에 라이브러리 패키지는 병합 시 바로 배포하는 전략(CI/CD)을 채택하고, 앱 패키지는 수동으로 배포를 트리거하도록 하였습니다.
각 앱 별로 배포 주기가 다르고 QA가 완료되어야만 상용배포 가능했기에 앱 패키지 배포의 경우 추가적인 장치가 필요했습니다. 이를 git tag를 이용한 태그 기반 배포로 해결했습니다.

text

Trunk에서의 태그 기반 배포 (출처)

코드 변경점의 경로에 따라 자동으로 라벨을 붙이고 라벨명을 참조하여 아래와 같은 컨벤션으로 trunk에 병합 시 태그를 자동으로 푸쉬하도록 하였습니다.

{package_name}/{version_number}

초기 구축을 완료하며

초기 구축은 첫 스텝이었습니다. 기존 프로젝트 이관 작업 및 모노레포 환경 개선 작업이 남아있었습니다. 한 달이라는 짧은 기간만이 주어진 상황이었기에 구축한 모노레포에서 기존 서비스 이관 전, 신규 서비스 런칭 작업을 우선적으로 진행하였습니다.
다음 글에선 초기 구축한 모노레포 환경에 어떤 문제가 있었고, 어떻게 개선해나갔는지에 대해 이어서 다뤄보도록 하겠습니다.

profile
기록의 아름다움

0개의 댓글

관련 채용 정보