모노레포 아키텍처: 레이어 기반 패키지 설계
지난 글에서 모노레포 도입 배경을 다뤘다면 이번에는 구체적인 아키텍처 설계를 살펴봅니다. 패키지 간 의존성을 어떻게 구조화해야 하는지, 그리고 왜 단방향 의존성 흐름이 유지보수의 핵심인지 설명합니다.
레이어 아키텍처 (Layered Architecture)
설계 원칙
디자인 시스템 내부의 패키지들은 엄격한 단방향 의존성을 갖도록 레이어로 구분했습니다. 이 구조는 복잡도를 낮추고 예측 가능한 시스템을 만드는 데 목적이 있습니다.
- 하위 레이어는 상위 레이어를 참조할 수 없음 - 순환 참조(Circular Dependency)를 원천 차단합니다.
- 같은 레이어 간 참조 지양 - 각 패키지의 독립성을 최대한 보장합니다.
- 상위 레이어만 하위 레이어를 참조 - 데이터와 로직의 흐름을 한 방향으로 유지합니다.
레이어 구조도
패키지별 역할 정의
| 패키지 | 레이어 | 핵심 역할 | 참조 가능 범위 |
|---|---|---|---|
@design-system/utils |
Layer 0 | 프레임워크에 종속되지 않는 순수 유틸리티 | 없음 (최하위) |
@design-system/theme |
Layer 0 | 색상, 타이포그래피 등 디자인 토큰 정의 | 없음 (최하위) |
@design-system/hooks |
Layer 1 | UI 로직을 담당하는 React Custom Hooks | utils, theme |
@design-system/components |
Layer 2 | 시각적 요소를 담당하는 UI 컴포넌트 | hooks, utils, theme |
@design-system/core |
Main | 라이브러리 배포를 위한 통합 진입점 | 모든 하위 패키지 |
디렉토리 구조
design-system/
├── packages/
│ ├── core/ # Main - 통합 진입점
│ │ ├── src/
│ │ │ └── index.ts # 모든 패키지 export
│ │ └── package.json
│ │
│ ├── components/ # Layer 2 - UI 컴포넌트
│ │ ├── src/
│ │ │ ├── atoms/ # 원자 단위 컴포넌트
│ │ │ ├── molecules/ # 분자 단위 컴포넌트
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── hooks/ # Layer 1 - React Hooks
│ │ ├── src/
│ │ │ ├── useToggle.ts
│ │ │ ├── useInput.ts
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── theme/ # Layer 0 - 테마 및 토큰
│ │ ├── src/
│ │ │ ├── tokens/ # 컬러, 스페이싱 등 정의
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ └── utils/ # Layer 0 - 유틸리티
├── src/
│ ├── cn.ts # 스타일 병합 유틸
│ ├── format.ts # 데이터 포맷터
│ └── index.ts
└── package.json
│
├── .changeset/ # 버전 관리 설정
├── scripts/ # 릴리즈 자동화 스크립트
├── .gitlab-ci.yml # CI 파이프라인 설정
└── eslint.config.mjs # 의존성 린트 설정
의존성 선언 전략
workspace 프로토콜 활용
모노레포 내부 패키지 간의 참조는 workspace:* 프로토콜을 사용합니다.
// packages/components/package.json
{
"name": "@design-system/components",
"dependencies": {
"@design-system/hooks": "workspace:*",
"@design-system/theme": "workspace:*",
"@design-system/utils": "workspace:*"
}
}
개발 중에는 항상 로컬 워크스페이스의 최신 소스 코드를 참조하므로 즉각적인 수정 사항 반영이 가능합니다. 이후 실제 배포 단계에서는 pnpm이 자동으로 시멘틱 버전(예: ^1.2.0)으로 변환해 줍니다.
배포 시 버전 변환
// 개발 환경 (workspace:*)
{
"dependencies": {
"@design-system/hooks": "workspace:*"
}
}
// 배포된 패키지 (실제 버전으로 치환됨)
{
"dependencies": {
"@design-system/hooks": "^0.2.1"
}
}
빌드 시스템 구성
Turborepo를 이용한 빌드 오케스트레이션
패키지 간의 의존성 순서를 보장하며 효율적으로 빌드하기 위해 Turborepo를 사용합니다.
// turbo.json
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}
"dependsOn": ["^build"] 설정은 매우 중요합니다. 이 설정 덕분에 "내가 의존하고 있는 패키지들을 먼저 빌드하라"는 규칙이 성립됩니다. 결과적으로 Layer 0 → Layer 1 → Layer 2 → Main 순서로 빌드가 자동 수행됩니다.
tsup 번들링
개별 패키지의 번들링은 설정이 간편하고 성능이 뛰어난 tsup을 활용합니다.
// packages/*/tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
splitting: true,
sourcemap: true,
clean: true,
treeshake: true,
external: ['react', 'react-dom', '@mui/material'],
});
- ESM + CJS 듀얼 지원 - 다양한 환경 대응
- d.ts 자동 생성 - 타입스크립트 타입 정의 포함
- Tree-shaking 최적화 -
sideEffects설정을 통해 불필요한 코드 제거
사용자 관점 (DX)
단일 진입점 배포 전략의 이점
최종 사용자에게는 오직 @design-system/core 패키지만 노출합니다. 내부적으로는 여러 패키지로 쪼개져 있지만 배포 단계에서는 하나의 패키지로 통합됩니다.
(이 방식은 팀 내 회의를 거쳐 최종 결정된 사안입니다.)
이 전략 덕분에 라이브러리를 사용하는 개발자는 복잡한 고민 없이 단 하나의 패키지만 설치하면 됩니다.
npm install @design-system/core
// 필요한 기능을 core에서 바로 import (Tree-shaking 작동)
import { Button, useToggle, theme, cn } from '@design-system/core';
이러한 배포 정책을 결정한 이유:
- 버전 관리 단순화 - 사용자는 core의 버전 하나만 신경 쓰면 됩니다.
- 호환성 이슈 방지 - 서로 다른 버전의 컴포넌트와 훅을 혼용해서 생기는 문제를 원천 차단합니다.
- 최적화 유지 - core가 모든 것을 export 하더라도, 모던 번들러들은 사용하지 않는 코드를 자동으로 제거(Tree-shaking)하므로 성능 손해가 없습니다.
components, hooks 같은 개별 패키지는 NPM Registry에 배포하지 않습니다(`private: true`). 이들은 오직 모노레포 내부 개발용으로만 존재합니다.
'je개발 회고' 카테고리의 다른 글
| [ 디자인 시스템 ] (3) 라이브러리 버전 관리 (0) | 2026.01.12 |
|---|---|
| [ 디자인 시스템 ] (2) 모노레포 의존성 관리 (0) | 2026.01.12 |
| [ 디자인 시스템 ] (0) 모노레포로 사내 라이브러리 만들기 (0) | 2026.01.12 |
| [ CI/CD ] 모노레포 Gitlab CI/CD 전략 (0) | 2025.12.22 |
| [ 번들러 ] 모노레포 번들러 비교 (0) | 2025.12.22 |