je개발 회고

[ 디자인 시스템 ] (1) 모노레포 아키텍처

Je-chan 2026. 1. 12. 13:46

모노레포 아키텍처: 레이어 기반 패키지 설계

지난 글에서 모노레포 도입 배경을 다뤘다면 이번에는 구체적인 아키텍처 설계를 살펴봅니다. 패키지 간 의존성을 어떻게 구조화해야 하는지, 그리고 왜 단방향 의존성 흐름이 유지보수의 핵심인지 설명합니다.

레이어 아키텍처 (Layered Architecture)

설계 원칙

디자인 시스템 내부의 패키지들은 엄격한 단방향 의존성을 갖도록 레이어로 구분했습니다. 이 구조는 복잡도를 낮추고 예측 가능한 시스템을 만드는 데 목적이 있습니다.

  1. 하위 레이어는 상위 레이어를 참조할 수 없음 - 순환 참조(Circular Dependency)를 원천 차단합니다.
  2. 같은 레이어 간 참조 지양 - 각 패키지의 독립성을 최대한 보장합니다.
  3. 상위 레이어만 하위 레이어를 참조 - 데이터와 로직의 흐름을 한 방향으로 유지합니다.

레이어 구조도

디자인 시스템 레이어 구조
Main (최상위 진입점)
@design-system/core
모든 패키지 Re-export
↑ depends on
Layer 2 (UI 구현)
@design-system/components
React UI 컴포넌트
↑ depends on
Layer 1 (비즈니스 로직)
@design-system/hooks
React Custom Hooks
↑ depends on
Layer 0 (기반)
@design-system/utils
순수 유틸 함수
(No Dependencies)
@design-system/theme
디자인 토큰 & 테마
(No Dependencies)

패키지별 역할 정의

패키지 레이어 핵심 역할 참조 가능 범위
@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:*"
  }
}
💡 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`). 이들은 오직 모노레포 내부 개발용으로만 존재합니다.