<aside> 🔗

GitHub 예시 코드

</aside>

<aside> 📄

이 글에서 모노레포 분석에 사용된 툴은 Turborepo 입니다.

</aside>

들어가는 말


“모노레포를 쓰면 효율이 좋다”는 말은 많이 들어왔다. 이전 회사의 메인 프로젝트들 역시 모노레포로 구성이 되어 있었다. 초기에는 멀티 레포로 시작되었지만 여러 서비스에서 공통으로 사용할 디자인 시스템을 도입하면서 모노레포도 함께 적용하였다.

그런데 이제 와서 “모노레포를 쓰면 효율이 좋다”는 말에 궁금증을 제기한 이유는, 사실 아직까지 완전히 납득하지는 못했기 때문이다. 당시에는 모노레포 도입을 모두가 당연히 받아들이는 분위기여서, 구체적으로 따져볼 기회용기가 없었다.

도대체 어디에, 어떻게, 왜 그렇게 좋다는 걸까. 단순히 여러 레포를 오가며 작업하지 않아도 돼서 좋은 걸까? 공통 패키지 재사용이 용이해서일까? 아니면 의존성 관리가 편리해서일까?

아무래도 “좋다”는 말로 뭉뚱그리기에는 내포할 수 있는 범위가 너무 넓은 것 같다.

그래서 이번 글에서 모노레포를 사용할 때 흔히 언급되는 장점들로 어떤 것들이 있는지, 또한 그들은 정말 장점이기만 한지에 대해서 기존의 멀티 레포와 비교하며 살펴보고자 한다.

살펴보기


코드 재사용 관점

멀티레포라고 해서 코드 재사용이 불가능했던 것은 아니다. 멀티레포에서는 공통 패키지를 별도의 레포에서 개발하고, 이를 npm에 배포한 뒤 애플리케이션 레포에서 설치해 사용한다. 즉, 배포와 설치 단계를 거쳐야 코드가 공유된다.

반면 모노레포는 훨씬 단순하다. packages 디텍토리 하위에 공통 패키지를 두고, 애플리케이션 코드에서 바로 불러서 쓰면 된다. 물리적으로 같은 레포에 있으니 코드 재사용이 자연스럽게 이뤄진다.

그런데, 여기서 중요한 건 “모노레포는 공통 패키지를 재사용하기 편하다”라는 표면적 사실보다는, “모노레포에서는 공통 패키지를 소스 레벨 그대로 불러와 사용한다”는 점이다.

공통 패키지가 빌드 과정을 거치지 않았다면, 애플리케이션은 해당 패키지를 빌드 결과물이 아닌 소스 코드 그대로 불러오게 된다. 이때 공통 패키지 내부에서 전처리가 필요한 라이브러리(예: Tailwind, Emotion, Vanilla Extract, svgr 등)를 사용 중이라면, 애플리케이션 환경에 전처리 도구가 있어야 한다.

예를 들어 Tailwind는 빌드 시점에 코드에서 사용된 클래스명을 스캔해 필요한 스타일을 주입한다.

그런데 애플리케이션에서 공통 패키지를 소스 레벨로 불러왔는데, 애플리케이션 빌드 환경에 Tailwind가 없다면? 당연히 해당 클래스명은 처리되지 않고, 스타일 역시 적용되지 않는다. 공통 패키지에서 Tailwind를 사용하면서 빌드 없이 소스 레벨로 가져다 쓰려면, 애플리케이션에도 Tailwind가 설치되어 있어야 한다. 결국 공통 패키지와 애플리케이션 사이에 의존성(독립성 부족)이 생긴 것이다.

예시 코드 1

/* packages/ui-tailwind/src/style.css */

@source './'; /* 현재 경로를 스캔하도록 설정 */
/* /apps/examples/app/globals.css */

@import 'tailwindcss'; /* 테일윈드 로드 */
@import '@repo/ui-tailwind/style.css'; /* UI 패키지 로드 */
/* /apps/examples/postcss.config.mjs */

export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

물론 공통 패키지를 빌드해 자체 독립성을 보장할수도 있다. 코드 변경마다 빌드 과정을 거쳐야 하는 번거로움이 있지만, watch 모드로 빌드를 구동시켜두면 어느정도 해결할 수 있다. (터보레포 공식 문서에 소개된 테일윈드 사용 방식)

예시 코드 2

/* /packages/ui-tailwind-build/package.json */

/* 컴포넌트에 사용된 클래스들의 처리를 위해 tailwind cli를 이용해 빌드한다. */

{
	// ...
	"exports": {
    "./*": "./dist/*.js",
    "./styles.css": "./dist/styles.css"
  },
  "scripts": {
    "dev:styles": "tailwindcss -i ./src/styles.css -o ./dist/styles.css --watch",
    "dev:components": "tsc --watch",
    "build:styles": "tailwindcss -i ./src/styles.css -o ./dist/styles.css",
    "build:components": "tsc"
  },
}
/* /packages/ui-tailwind-build/dist/styles.css */

/* 개발에 사용된 스타일들이 빌드 결과물로 포함된다. */

@layer utilities {
  .bg-red-200 {
    background-color: var(--color-red-200);
  }
  .p-2 {
    padding: calc(var(--spacing) * 2);
  }
  .text-2xl {
    font-size: var(--text-2xl);
    line-height: var(--tw-leading, var(--text-2xl--line-height));
  }
  .font-semibold {
    --tw-font-weight: var(--font-weight-semibold);
    font-weight: var(--font-weight-semibold);
  }
}
/* /apps/examples/app/globals.css */

/* 단일 CSS만 불러오면 된다. */
@import '@repo/ui-tailwind-build/styles.css';

모노레포는 멀티레포에 비해 같은 레포 안에 있기 때문에 재사용이 쉽다는 점이 분명 강점이다. 하지만 그 재사용이 소스 레벨에서의 재사용인지, 빌드 결과물의 재사용인지는 반드시 구분해야 한다. “모노레포라서 편하다”는 말 뒤에, 실제로 어떤 형태로 코드를 재사용하고 있는지를 알아야 한다.

버전 관리 관점

멀티레포에서 설치한 패키지의 버전은 설치 시점의 버전으로 고정된다. 이후 패키지가 업데이트되더라도, 애플리케이션에서 직접 업데이트하지 않는 한 사용 중인 패키지 버전은 변하지 않는다.

반면에 모노레포에서 설치한 (packages 하위) 패키지의 버전은 실시간으로 추종된다. 공통 패키지가 업데이트 되면, 애플리케이션에서도 즉시 최신 코드가 반영된다.

패키지 업데이트 방식의 차이는 상황에 따라 서로가 장점이 될 수도, 단점이 될 수도 있다.

예를 들어, 서비스별 관리자가 다르고 각자가 작업 타이밍을 독립적으로 조율해야 한다면 멀티레포 방식이 더 효율적일 수 있다. 반대로 모노레포 방식은 개인의 스케줄 제어권이 줄고, 팀 전체가 “동시 대응”을 강제받는 구조가 될 수 있다.

반대로, 공통 코드가 자주 바뀌어 빠른 피드백이 필요한 경우 모노레포 방식이 효율적일 수 있다. 모노레포에서는 애플리케이션 코드가 패키지의 최신 변경사항을 즉시 반영하지만, 멀티레포에서는 코드를 수정할 때마다 배포-설치-적용 과정을 반복해야 하는 데다가, 서비스가 여러 개라면 그 작업을 모두 반복해서 수행해야 하기 때문이다.