그순간

모노레포(Monorepo) 제대로 이해하기

김서연·2026년 10월 3일·26 조회
모노레포(Monorepo) 제대로 이해하기

프론트엔드 프로젝트를 여러 개 운영하다 보면 비슷한 문제를 한 번쯤 만나게 됩니다.

여러 서비스에서 사용하는 컴포넌트가 조금씩 달라지고, API 타입이나 유틸 함수도 중복해서 관리하게 됩니다. ESLint나 TypeScript 설정을 변경하려고 해도 프로젝트마다 따로 수정해야 합니다.

처음에는 각각의 프로젝트를 별도의 Git Repository로 관리하는 방식이 자연스럽습니다.
하지만 프로젝트가 많아질수록 공통 코드를 어떻게 관리할 것인지, 프로젝트 간 변경사항을 어떻게 함께 처리할 것인지가 점점 중요해집니다.

이때 선택지 중 하나가 Monorepo(모노레포) 입니다.

이번 글에서는 모노레포가 무엇인지부터 시작해서 Polyrepo와의 차이, pnpm Workspace, Turborepo, 의존성 그래프와 캐싱, CI/CD까지 순서대로 정리해보겠습니다.
실제 프로젝트에서 사용하는 구조도 함께 살펴보면서 단순히 모노레포가 단순히 여러 프로젝트를 한 저장소에서 관리하는 방식이라는 설명에서 끝나지 않도록 정리해보겠습니다.


1. 모노레포란?

Monorepo는 Monolithic Repository의 줄임말로, 여러 프로젝트와 패키지를 하나의 Git Repository에서 관리하는 구조를 의미합니다.
monolithic: 단단히 짜여 하나로 되어 있는

예를 들어 하나의 서비스만 운영한다면 다음과 같은 구조로도 충분합니다.

project/
├── src/
├── public/
├── package.json
└── ...

하지만 하나의 조직에서 여러 서비스를 운영한다고 생각해보겠습니다.

client
admin
landing
design-system
shared-api

이를 각각 Repository로 관리할 수도 있습니다.

client-repository
admin-repository
landing-repository
design-system-repository
shared-api-repository

이러한 방식을 일반적으로 Polyrepo라고 합니다.
반대로 모노레포에서는 여러 프로젝트를 하나의 Repository 안에서 관리합니다.

datagsm-client/
├── apps/
│   ├── client/
│   ├── admin/
│   └── landing/
│
└── packages/
    ├── ui/
    ├── api/
    └── config/

즉, 중요한 것은 단순히 디렉터리가 여러 개 존재한다는 것이 아닙니다. 서로 독립적으로 실행될 수 있는 여러 애플리케이션과 공통 패키지를 하나의 저장소에서 함께 관리한다는 것이 모노레포의 핵심이라고 생각하시면 될 것 같습니당


2. Monorepo와 Monolith의 차이점

모노레포를 처음 접했을 때 가장 많이 헷갈리는 부분입니다. Monorepo와 Monolith는 이름이 비슷하지만 서로 다른 개념입니다.

Monolith

Monolith는 애플리케이션의 구조에 대한 개념입니다.
하나의 애플리케이션이 여러 기능을 하나의 배포 단위로 가지고 있는 구조를 의미합니다.

Monolith
└── 하나의 Application
    ├── User
    ├── Payment
    ├── Order
    └── Admin

Monorepo

Monorepo는 코드를 어떻게 저장소에서 관리할 것인지에 대한 개념입니다.

Monorepo
├── Application A
├── Application B
├── Application C
└── Shared Packages

따라서 Monorepo 안에 여러 개의 Monolith 애플리케이션이 존재할 수도 있고, 여러 Microservice를 관리할 수도 있습니다.
즉, 이 둘은 서로 대체 관계라기 보다는, Monolith는 애플리케이션 아키텍처에 대한 개념이고, Monorepo는 저장소 관리 방식에 대한 개념입니다. 이것만 기억하시면 될 것 같아요


3. 왜 모노레포가 필요할까?

가장 단순한 상황부터 생각해보겠습니다.

client와 admin이라는 두 개의 애플리케이션이 있고 둘 모두 같은 API 타입을 사용한다고 가정하겠습니다.
Polyrepo라면 다음과 같은 구조가 될 수 있습니다.

client-repo
└── types/
    └── User.ts

admin-repo
└── types/
    └── User.ts

처음에는 문제가 없습니다.
하지만 서버의 User 타입이 변경되면 두 Repository의 코드를 모두 수정해야 합니다.

User
├── client → 수정
└── admin  → 수정

그리고 두 Repository의 버전 차이로 서로 달라질 가능성도 생기겠죠
모노레포에서는 공통 코드를 별도의 패키지로 만들 수 있습니다.

repository/
├── apps/
│   ├── client/
│   └── admin/
│
└── packages/
    └── api/
        └── User.ts

그러면 두 애플리케이션이 같은 패키지를 참조할 수 있습니다.

             ┌── client
             │
@repo/api ───┤
             │
             └── admin

공통 코드의 변경도 하나의 Repository에서 함께 관리할 수 있습니다.

저는 위 예시들이 모노레포의 대표적인 장점이라고 생각합니다.


4. Monorepo의 장점

코드 공유

가장 먼저 떠올릴 수 있는 장점인데요, 공통 UI 컴포넌트, API 함수, 타입, 유틸리티 등을 별도의 package로 만들고 여러 애플리케이션에서 사용할 수 있습니다.

packages/
├── ui/
├── api/
├── hooks/
├── constants/
└── utils/

예를 들어 @repo/ui에 Button을 만들어 두면 여러 앱에서 사용할 수 있습니다.

import { Button } from "@repo/ui";

공통 UI를 하나의 위치에서 관리할 수 있기 때문에 중복 구현을 줄일 수 있습니다.


변경사항 함께 관리 가능

모노레포에서는 하나의 변경사항이 여러 애플리케이션에 영향을 주는 경우에도 하나의 Pull Request에서 관리할 수 있습니다.
예를 들어 API 응답 타입을 변경한다고 가정해보겠습니다.

packages/api
      ↓
  API 타입 변경
      ↓
 ┌────┴────┐
 ↓         ↓
client    admin

Polyrepo라면 여러 Repository의 PR을 각각 만들어야 할 수도 있습니다.
반면 모노레포에서는 관련 변경사항을 하나의 PR에서 확인할 수 있습니다.

이러한 부분들은 특히 API 타입과 실제 사용하는 클라이언트 코드의 변경을 함께 관리해야 할 때 유용합니다.


개발 환경 통일

ESLint, Prettier, TypeScript 등의 설정을 공통 패키지로 관리할 수 있습니다.

packages/
├── eslint-config/
├── typescript-config/
└── prettier-config/

각 애플리케이션은 공통 설정을 가져와 사용할 수 있습니다.
이렇게 하면 프로젝트마다 조금씩 다른 설정이 생기는 것을 줄일 수 있습니다.


5. Monorepo의 단점?

여기서 생각해봐야할건 모노레포가 항상 좋은 선택인 것은 아니라는겁니다.
Repository가 하나로 합쳐지는 만큼 관리해야 하는 복잡도도 같이 올라갑니다.

대표적으로 다음과 같은 문제가 있습니다.

  • Repository의 규모 증가
  • Dependency 관계 복잡
  • CI/CD 설정이 복잡해질 수 있음
  • Build 시간이 길어질 수 있음(최적화 필요)
  • 패키지 간 의존성 방향을 잘못 설계하면 순환 의존성이 생길 수 있음
  • 처음 접하는 팀원에게 구조가 익숙하지 않을 수 있음

특히 프로젝트가 커질수록 어떤 코드를 어느 package에 넣어야 하는지가 중요해집니다.
모노레포라고 해서 모든 코드를 packages/common 같은 하나의 패키지에 넣어버리면 오히려 관리하기 어려워집니다.

따라서 모노레포에서는 Package Boundary를 어떻게 설계할 것인지가 중요합니다.


6. Monorepo의 기본 구조

일반적인 프론트엔드 모노레포는 다음과 같은 형태로 구성할 수 있습니다.

project/
├── apps/
│   ├── client/
│   └── admin/
│
├── packages/
│   ├── ui/
│   ├── api/
│   ├── hooks/
│   ├── constants/
│   └── eslint-config/
│
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── pnpm-lock.yaml

여기서 크게 두 가지 영역으로 나눌 수 있습니다.

apps

실제로 실행되고 배포되는 애플리케이션입니다.

apps/
├── client/
└── admin/

예를 들어 Next.js 애플리케이션이라면 각각 별도의 Next.js Application이 됩니다.

packages

여러 애플리케이션에서 공유하기 위한 패키지입니다.

packages/
├── ui/
├── api/
├── hooks/
└── constants/

따라서 일반적으로 다음과 같은 방향으로 생각할 수 있습니다.

                Monorepo
                   │
          ┌────────┴────────┐
          │                 │
        apps             packages
          │                 │
     ┌────┴────┐       ┌────┴─────┐
     │         │       │          │
   client    admin    api        ui

이 구조를 시각적으로 이해하고 넘어가면 이후에 나오는 Workspace와 Turborepo를 이해하기 훨씬 쉽습니다.


7. Workspace란?

여기서 한 가지 문제가 있을 수 있습니다.

Repository 안에 프로젝트가 여러 개 있다고 해서 패키지들이 자동으로 연결되는 것은 아니라는 점인데요,
예를 들어 다음과 같은 구조가 있다고 해보겠습니다.

apps/client
packages/api

client에서 api를 사용하려면 두 패키지가 서로 Workspace에 속해 있다는 것을 패키지 매니저에게 알려줘야 하는데,
이 역할을 하는 것이 Workspace입니다.

pnpm에서는 pnpm-workspace.yaml을 통해 Workspace를 정의할 수 있습니다.

packages:
  - "apps/*"
  - "packages/*"

복잡하게 생각할건 없고, 이 설정은 apps와 packages 아래에 있는 프로젝트들을 하나의 Workspace로 관리하겠다는 의미입니다.


8. pnpm Workspace

제가 모노레포에서 자주 사용하는 방식 중 하나가 pnpm Workspace입니다.
예를 들어 다음과 같은 패키지가 있다고 하겠습니다.

apps/client
packages/api

client/package.json에서 다음과 같이 선언할 수 있습니다.

{
  "dependencies": {
    "@repo/api": "workspace:*"
  }
}

여기서 workspace:*가 중요한 역할을 합니다.
일반적인 npm dependency라면 Registry에서 패키지를 찾아 설치할 수 있지만,

"@repo/api": "^1.0.0"

Workspace dependency에서는 현재 Repository 안에 있는 패키지를 연결합니다.

"@repo/api": "workspace:*"

쉽게 설명하자면

apps/client
      │
      │ workspace:*
      ▼
packages/api

와 같은 관계가 만들어집니다.


9. pnpm을 사용하는 이유

추가적으로 모노레포에서는 npm, Yarn, pnpm 등 여러 패키지 매니저를 사용할 수 있습니다.
그중 pnpm은 Workspace와 함께 사용하기 좋은 기능들을 제공합니다.

특히 pnpm의 dependency 관리 방식을 이해하려면 node_modules를 단순히 생각하면 안 됩니다.
npm에서는 일반적으로 프로젝트마다 dependency가 설치됩니다.

반면 pnpm은 content-addressable store를 사용하여 패키지를 효율적으로 저장하고, 프로젝트에서는 필요한 패키지를 연결하는 방식을 사용합니다. 이 때문에 동일한 패키지를 여러 프로젝트에서 사용하더라도 중복 저장을 줄일 수 있습니다.

또한 pnpm은 dependency를 엄격하게 관리하기 때문에 프로젝트에서 직접 선언하지 않은 패키지를 우연히 사용할 수 있는 phantom dependency 문제를 줄이는 데에도 도움이 됩니다.


10. Turborepo는 왜 필요할까?

여기까지 보면 pnpm Workspace만 있으면 모노레포를 사용할 수 있는 것 아닌가? 라고 생각이 들 수 있는데요, 결론적으로는 모노레포를 구성하는 데 Turborepo가 반드시 필요한 것은 아닙니다.
pnpm Workspace만으로도 여러 프로젝트와 패키지를 하나의 Repository에서 관리할 수 있습니다.

그렇다면 Turborepo는 왜 사용할까요?
대표적인 이유가 Task Orchestration과 Caching입니다.

예를 들어 Repository 안에 다음 프로젝트가 있다고 하겠습니다.

apps/
├── client/
└── admin/

packages/
├── api/
└── ui/

각 프로젝트에는 다음과 같은 script가 있을 수 있습니다.

{
  "scripts": {
    "build": "next build",
    "lint": "next lint",
    "test": "..."
  }
}

Repository 전체의 build를 실행하려면 모든 프로젝트의 build를 관리해야 합니다.
Turborepo를 사용하면 다음처럼 실행할 수 있습니다.

turbo run build

그러면 Turborepo가 Workspace 안의 package와 dependency 관계를 바탕으로 어떤 task를 어떤 순서로 실행할지 관리합니다.


11. Dependency Graph

Turborepo를 이해하려면 Dependency Graph를 알아야 합니다.
예를 들어 다음과 같은 관계가 있다고 해보겠습니다.

client
  │
  ├── api
  │
  └── ui

그리고 admin도 api를 사용한다고 하면,

          api
         /   \
        /     \
    client    admin
       │
       └── ui

이런 그래프가 만들어집니다.

여기서 중요한 것은 모든 package를 무조건 동시에 실행하는 것이 아니라 의존 관계를 고려하여 task 실행 순서를 결정한다는 것입니다.

예를 들어 client의 build가 api의 build 결과에 의존한다면 api가 먼저 처리되어야 합니다.
이러한 관계를 코드로 정의할 수 있습니다.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"]
    }
  }
}

여기서 ^build는 현재 package가 의존하고 있는 workspace package의 build task도 먼저 실행한다는 의미입니다.

따라서 단순히 build를 실행하는 게 아니라

turbo run build
        │
        ▼
Dependency Graph 분석
        │
        ▼
의존 package build
        │
        ▼
현재 package build

와 같은 흐름으로 이해할 수 있습니다.


12. Turborepo의 Cache

모노레포에서 가장 유용한 기능 중 하나가 Cache입니다.
예를 들어 Repository에 다음과 같은 프로젝트가 있다고 하겠습니다.

apps/
├── client
└── admin

어제 client의 build를 실행했고 결과가 캐시에 저장되어 있다고 가정하겠습니다.
오늘 admin의 코드만 수정한 뒤 다시 전체 build를 실행하면 client까지 무조건 다시 build할 필요가 없습니다. 왜냐하면 변경사항과 입력값이 이전과 동일하다면 기존 결과를 사용할 수 있기 때문입니다.

turbo run build
       │
       ├── client
       │     └── Cache Hit
       │
       └── admin
             └── Cache Miss
                    ↓
                  build

이것이 모노레포에서 Cache가 중요한 이유입니다.


13. Cache Hit와 Cache Miss

Turborepo는 Task의 입력값을 기반으로 결과를 재사용할 수 있는지 판단합니다.
대표적으로 다음과 같은 것들이 영향을 줄 수 있습니다.

  • 소스 코드
  • package.json
  • lockfile
  • 환경변수
  • 설정 파일
  • dependency 변경

입력이 동일하다면 이전 결과를 재사용할 수 있습니다.

Input
 │
 ├── source
 ├── package.json
 ├── lockfile
 └── config
       │
       ▼
    Hash 생성
       │
       ▼
   Cache 확인
    /       \
 Hit       Miss
  │           │
  ▼           ▼
사용        Build
결과          │
              ▼
            Cache 저장

이 구조를 이해하면 왜 코드를 수정하지 않았는데도 build가 다시 실행되는지와 같은 문제를 분석할 때도 도움이 될 것 같습니당


14. 실제 프로젝트의 Monorepo 구조

실제 Hello, GSM 프로젝트에서는 다음과 같은 형태로 모노레포를 구성할 수 있습니다.

hellogsm-client-26/
├── apps/
│   ├── client/
│   └── admin/
│
├── packages/
│   ├── api/
│   ├── constants/
│   ├── eslint-config/
│   ├── hooks/
│   ├── ocr-lambda/
│   └── store/
│
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json

여기서 apps에는 실제 서비스를 실행하는 애플리케이션을 두고, packages에는 여러 애플리케이션에서 공유하거나 독립적으로 관리할 수 있는 기능을 분리합니다.

예를 들어,

@repo/api

는 API 관련 코드,

@repo/constants

는 공통 상수,

@repo/hooks

는 공통 React Hook,

@repo/store

는 상태 관리와 관련된 코드를 담당하는 식입니다. 이렇게 책임을 분리하면 하나의 거대한 common 패키지가 생기는 것을 방지할 수 있습니다.


15. Package는 언제 분리해야 할까?

모노레포를 사용한다고 해서 코드를 무조건 여러 package로 나누는 것도 좋은 방법은 아닙니다.
예를 들어 다음과 같은 구조가 있다고 해보겠습니다.

packages/
└── common/
    ├── api/
    ├── hooks/
    ├── store/
    ├── constants/
    └── utils/

처음에는 편해 보이지만 시간이 지나면서 common이 모든 코드가 들어가는 거대한 패키지가 될 가능성이 있습니다.

반대로 너무 작은 단위로 쪼개도 관리 비용이 증가합니다.
따라서 Package를 분리할 때는 변경 주기와 책임, 의존 관계를 기준으로 생각하는 것이 좋습니다.
예를 들어 UI 컴포넌트가 여러 앱에서 공유된다면 ui package로 분리할 수 있습니다.

packages/
└── ui/
    ├── Button
    ├── Input
    └── Modal

반면 특정 앱에서만 사용하는 컴포넌트라면 굳이 package로 분리할 필요가 없습니다.

apps/client/
└── components/
    └── UserProfile/

결국 중요한 것은 "얼마나 많이 분리했는가"가 아니라 각 package가 명확한 책임을 가지고 있는가입니다.


16. CI/CD에서 Monorepo

모노레포는 CI/CD에서도 장점과 단점이 동시에 나타납니다. 가장 단순하게 생각하면 PR이 생성될 때 다음 작업을 실행할 수 있습니다.

Pull Request
      │
      ▼
   Install
      │
      ▼
     Lint
      │
      ▼
    Build
      │
      ▼
     Test
      │
      ▼
   Deploy

문제는 Repository 안에 앱이 많아질수록 모든 앱을 매번 검사하는 비용이 커진다는 것입니다.
그래서 모노레포에서는 변경된 package와 그 영향을 받는 package를 파악하는 것이 중요합니다.

예를 들어,

packages/api
      │
      ├── client
      └── admin

api가 변경됐다면 client와 admin 모두 영향을 받을 가능성이 있습니다.
반면 admin만 수정했다면 client까지 다시 build해야 할 이유는 없을 수 있습니다.
이때 Dependency Graph와 Cache가 CI 시간을 줄이는 데 도움을 줄 수 있습니다.


17. 모노레포에서 자주 발생하는 문제

모노레포를 실제로 운영하다 보면 몇 가지 문제가 반복해서 등장합니다.

순환 의존성

예를 들어,

api → utils
utils → api

와 같은 구조가 만들어지면 서로가 서로를 의존하게 됩니다.
의존성 방향을 명확하게 설계하는 것이 중요하기 때문에 이런 구조는 가능한 한 피해야 한다고 생각합니다.


Package Boundary 붕괴

처음에는 각 package의 책임이 명확했지만 시간이 지나면서 다른 package의 내부 구현을 직접 참조하기 시작할 수 있습니다.

client
  ↓
store
  ↓
api
  ↓
utils

이런 관계가 계속 늘어나면 package 간 결합도가 높아집니다.
모노레포에서는 코드를 쉽게 공유할 수 있다는 장점이 오히려 강한 결합으로 이어질 수 있습니다.

따라서 "공유할 수 있다"와 "공유해야 한다"는 다른 문제라는 점을 기억해야 합니다.


Cache 문제

제가 실재로 자주 겪었던 문제인데, 캐시가 제대로 설정되지 않으면 다음과 같은 상황도 발생할 수 있습니다.

코드 변경
   ↓
Build
   ↓
예상하지 못한 Cache Hit
   ↓
이전 결과 사용

반대로 입력값이 제대로 정의되지 않아 항상 Cache Miss가 발생할 수도 있습니다.
따라서 Turborepo의 Cache를 사용할 때는 어떤 파일과 환경변수가 Task의 입력인지 이해해야 합니다.


18. 실제로 모노레포를 사용하면서 중요하다고 느낀 점

모노레포를 처음 접하면 가장 먼저 눈에 들어오는 것은 디렉터리 구조인데요

apps/
packages/

하지만 실제로 중요한 것은 이 구조 자체가 아니라 모노레포에서 중요한건 결국 의존성을 어떻게 관리하느냐에 있다고 생각합니다

예를 들어,

client → api → constants

라는 관계가 있다면 각각의 package가 어떤 책임을 가지고 있는지 명확해야 합니다.
그리고 프로젝트가 커질수록 다음 세 가지가 중요해집니다.

Package Boundary
       +
Dependency Graph
       +
Build / Cache

Package Boundary가 명확해야 코드가 무분별하게 공유되지 않고, Dependency Graph가 명확해야 빌드 순서를 관리할 수 있습니다.
그리고 Build와 Cache를 잘 구성해야 모노레포의 규모가 커져도 개발 및 CI 비용이 지나치게 증가하는 것을 막을 수 있습니다.


마무리

이번에 모노레포에 대한 질문을 받았는데, 실제로 사용하고 있으면서도 왜 사용하고 어떤 문제가 있을지 제대로 설명하지 못했습니다.🥲
그래서 이번 기회에 제가 사용하고 있던 모노레포를 처음부터 다시 찾아봤습니다.

정리하면서 pnpm Workspace가 여러 프로젝트를 하나의 Workspace로 관리하는 역할을 하고, Turborepo는 그 위에서 Task 실행과 Dependency Graph, Cache를 관리한다는 것을 알게 되었습니다. 또 프로젝트가 커질수록 package를 어떻게 나누고 서로의 의존성을 어떻게 관리할지 정하는 것도 중요하다는 것을 알게 되었습니다.

결국 모노레포는 단순히 여러 프로젝트를 한 Repository에 넣는 것이 아니라, 여러 프로젝트 사이의 관계를 어떻게 관리할 것인지에 대한 구조라는 것을 기억하면 좋을 것 같습니다!

목차