디자인시스템 도입 가이드

Shin Jinseop·2026년 3월 17일

목적: ui 컴포넌트를 UX팀과 커뮤니케이션하기 위한 Storybook 환경 구축
스택: pnpm + Turborepo + React + TypeScript + Vite


A. ui에 Storybook 넣기 (Vite 기반)

ui가 이미 Vite로 구성되어 있으므로, Storybook은 @storybook/react-vite 프레임워크를 사용하여 기존 vite.config.ts를 그대로 활용한다.

A-1. 설치

cd ui
pnpm dlx storybook@latest init --type react

init이 자동으로 Vite 프로젝트를 감지하여 @storybook/react-vite를 설치한다.
만약 감지하지 못하면 수동 설치:

pnpm add -D storybook @storybook/react-vite @storybook/addon-essentials @storybook/addon-a11y

자동 생성되는 것들:

  • .storybook/main.ts — Storybook 설정
  • .storybook/preview.ts — 글로벌 데코레이터, 파라미터
  • src/stories/ — 예시 스토리 (삭제해도 됨)

A-2. .storybook/main.ts 설정

import type { StorybookConfig } from "@storybook/react-vite";
import { mergeConfig } from "vite";

const config: StorybookConfig = {
  stories: ["../src/**/*.stories.@(ts|tsx)"],
  addons: [
    "@storybook/addon-essentials",
    "@storybook/addon-a11y",
  ],
  framework: {
    name: "@storybook/react-vite",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
  // 핵심: 기존 vite.config.ts를 Storybook이 자동으로 로드한다.
  // 추가 커스텀이 필요한 경우에만 viteFinal을 사용.
  viteFinal: async (config) => {
    return mergeConfig(config, {
      // 예: 모노레포 심볼링크 resolve 이슈가 있을 때
      // resolve: {
      //   dedupe: ["react", "react-dom"],
      // },
    });
  },
};

export default config;

Storybook + Vite 동작 원리

@storybook/react-vite는 ui의 vite.config.ts를 자동 로드한다.
즉, 기존에 설정한 path alias, plugins, resolve 등이 Storybook에도 그대로 적용된다.
viteFinal은 Storybook 전용으로 추가 설정이 필요할 때만 쓴다 (덮어쓰기가 아닌 merge).

A-3. 기존 vite.config.ts와의 관계

ui/
├── vite.config.ts         ← 기존 Vite 설정 (라이브러리 빌드용)
├── .storybook/
│   ├── main.ts            ← Storybook이 vite.config.ts를 자동 로드 + viteFinal로 확장
│   └── preview.ts         ← 글로벌 데코레이터

주의할 점:

항목설명
path aliasvite.config.ts에 resolve.alias 설정이 있으면 Storybook에도 자동 적용됨
pluginsvite.config.ts의 @vitejs/plugin-react 등도 자동 적용됨
env 변수.env 파일도 Vite 규칙대로 자동 로드됨
빌드 설정build.lib 같은 라이브러리 빌드 설정은 Storybook 빌드 시 무시됨 (Storybook이 자체 빌드 파이프라인 사용)

만약 vite.config.ts에 라이브러리 빌드(build.lib)와 Storybook이 충돌하는 경우:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": "/src",
    },
  },
  // build.lib 설정은 Storybook이 자동으로 무시하므로 충돌 없음
  build: {
    lib: {
      entry: "src/index.ts",
      formats: ["es", "cjs"],
    },
    rollupOptions: {
      external: ["react", "react-dom"],
    },
  },
});

A-4. .storybook/preview.ts 설정

import type { Preview } from "@storybook/react";

// ui의 글로벌 스타일이 있다면 여기서 import
// import "../src/styles/global.css";

const preview: Preview = {
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    options: {
      storySort: {
        order: ["Foundation", "Components", "Patterns"],
      },
    },
  },
};

export default preview;

A-5. 스토리 파일 배치

ui/
├── vite.config.ts
├── .storybook/
│   ├── main.ts
│   └── preview.ts
├── src/
│   ├── Button/
│   │   ├── Button.tsx
│   │   ├── Button.stories.tsx   ← 컴포넌트 옆에 배치
│   │   └── index.ts
│   ├── Input/
│   │   ├── Input.tsx
│   │   ├── Input.stories.tsx
│   │   └── index.ts
│   └── index.ts                 ← 라이브러리 엔트리포인트

A-6. 스토리 작성 예시

// src/Button/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta = {
  title: "Components/Button",
  component: Button,
  tags: ["autodocs"],
  argTypes: {
    variant: {
      control: "select",
      options: ["primary", "secondary", "ghost"],
      description: "버튼 스타일 변형",
    },
    size: {
      control: "select",
      options: ["sm", "md", "lg"],
      description: "버튼 크기",
    },
    disabled: {
      control: "boolean",
      description: "비활성화 상태",
    },
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    variant: "primary",
    children: "확인",
  },
};

export const Secondary: Story = {
  args: {
    variant: "secondary",
    children: "취소",
  },
};

export const AllVariants: Story = {
  render: () => (
    <div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="primary" disabled>Disabled</Button>
    </div>
  ),
};

A-7. ui/package.json 스크립트 추가

{
  "name": "@myorg/ui",
  "scripts": {
    "dev": "vite",                                    // 기존
    "build": "vite build",                            // 기존
    "storybook": "storybook dev -p 6006",             // 추가
    "build-storybook": "storybook build -o storybook-static"  // 추가
  },
  "devDependencies": {
    "vite": "^6.x",                        // 기존
    "@vitejs/plugin-react": "^4.x",        // 기존
    "storybook": "^8.x",                   // 추가
    "@storybook/react-vite": "^8.x",       // 추가
    "@storybook/addon-essentials": "^8.x",  // 추가
    "@storybook/addon-a11y": "^8.x"        // 추가
  }
}

A-8. .gitignore 추가

# ui/.gitignore
storybook-static/

A-9. 트러블슈팅

증상원인해결
Cannot find module 'react'모노레포에서 react가 hoist 안 됨viteFinal에서 resolve.dedupe: ["react", "react-dom"] 추가
path alias (@/) 안 먹힘vite.config.ts 로드 실패storybook dev 로그에서 Using Vite config 확인
CSS Modules 스타일 안 나옴Vite 기본 지원이므로 별도 설정 불필요.module.css 확장자 확인
HMR 안 됨Storybook Vite 포트 충돌storybook dev -p 6007로 포트 변경
build.lib 관련 에러Storybook 빌드와 충돌 (드문 케이스)viteFinal에서 build.lib 명시적 제거: { build: { lib: undefined } }

B. AWS Amplify로 배포

B-1. 왜 Amplify인가

옵션장점단점
AmplifyGit push만으로 자동 배포, PR별 프리뷰, 무료 티어AWS 계정 필요
Vercel빠른 설정무료 티어 제한
ChromaticStorybook 특화 (비주얼 테스트)유료
GitHub Pages무료수동 설정 필요, PR 프리뷰 없음

Amplify는 정적 사이트 배포에 무료 티어가 넉넉하고, PR별 프리뷰 URL을 자동 생성해서 UX팀에게 링크 공유하기 좋다.

B-2. Amplify 콘솔에서 설정

  1. AWS Console → Amplify → "New app" → "Host web app"
  2. Git 저장소 연결 (GitHub / CodeCommit / Bitbucket)
  3. 모노레포 설정:
    • Monorepo root directory: ui

B-3. amplify.yml (저장소 루트에 생성)

version: 1
applications:
  - appRoot: ui
    frontend:
      phases:
        preBuild:
          commands:
            - cd ../../
            - npm install -g pnpm
            - pnpm install --frozen-lockfile
        build:
          commands:
            - cd ../../
            - pnpm --filter @myorg/ui build-storybook
      artifacts:
        baseDirectory: storybook-static
        files:
          - "**/*"
      cache:
        paths:
          - ../../node_modules/**/*
          - node_modules/**/*

주의: appRootui로 설정하면 Amplify가 해당 디렉토리에서 빌드를 시작한다.
pnpm install은 모노레포 루트에서 해야 하므로 cd ../../로 이동이 필요하다.

B-4. 환경 변수 (Amplify 콘솔)

PNPM_VERSION = 9
NODE_OPTIONS = --max-old-space-size=4096

B-5. PR 프리뷰 활성화

Amplify 콘솔 → App settings → Previews → Enable previews

  • main push → 메인 Storybook URL 자동 배포
  • PR 생성 → PR별 프리뷰 URL 생성 → UX팀에게 공유 가능

B-6. 접근 제어 (선택)

Amplify 콘솔 → Access control → Manage access

  • 비밀번호 보호 설정 가능 (회사 외부 접근 차단)
  • 또는 Amplify + Cognito 연동으로 팀원만 접근 가능하게 설정

C. 루트에서 관리하는 방법

C-1. Turborepo 파이프라인 설정

// turbo.json (루트)
{
  "tasks": {
    "build-storybook": {
      "dependsOn": ["^build"],
      "outputs": ["storybook-static/**"]
    },
    "storybook": {
      "dependsOn": ["^build"],
      "cache": false,
      "persistent": true
    }
  }
}

C-2. 루트 package.json 스크립트

// package.json (루트)
{
  "scripts": {
    "storybook": "turbo run storybook --filter=@myorg/ui",
    "build-storybook": "turbo run build-storybook --filter=@myorg/ui"
  }
}

루트에서:

pnpm storybook          # 로컬 개발 서버
pnpm build-storybook    # 정적 빌드 (배포용)

C-3. 의존성 관리 전략

Storybook 관련 의존성은 ui의 devDependencies에만 둔다.

루트/
├── package.json          ← storybook 의존성 없음 (스크립트만)
├── turbo.json            ← 파이프라인 정의
├── pnpm-workspace.yaml
├── amplify.yml           ← 배포 설정
└── ui/
 ├── vite.config.ts  ← 기존 Vite 설정 (Storybook이 자동 로드)
 ├── package.json    ← storybook devDependencies 여기
 ├── .storybook/     ← storybook 설정 여기
 └── src/
   └── Button/
    ├── Button.tsx
    └── Button.stories.tsx

루트에 storybook 의존성을 올리면 다른 패키지까지 오염될 수 있다.
turbo의 --filter가 해당 패키지의 의존성만 설치/빌드하게 해주므로
관심사 분리가 유지된다.

C-4. CI에서의 활용 (GitHub Actions 예시)

# .github/workflows/storybook.yml
name: Storybook

on:
  push:
    branches: [main]
    paths:
      - "ui/**"
  pull_request:
    paths:
      - "ui/**"

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "pnpm"
      - run: pnpm install --frozen-lockfile
      - run: pnpm build-storybook

Amplify가 Git 연동 배포를 하므로 CI에서는 빌드 성공 여부만 검증.


요약: UX팀 커뮤니케이션 흐름

개발자가 컴포넌트 수정
  → PR 생성
  → Amplify가 프리뷰 URL 자동 생성
  → UX팀에게 프리뷰 URL 공유
  → Storybook에서 컴포넌트 확인 + Controls로 직접 조작
  → 피드백 → PR에 코멘트
  → 머지 → 메인 Storybook URL 자동 업데이트

0개의 댓글