프론트엔드 Storybook 배포

2025년 11월 30일

개발프론트엔드

0. 시작하기 전에

회사 프로젝트를 진행 하기 직전 갑작스런 인력 변화가 있었고, 그로 인해 제대로 된 디자인 없이 프로젝트를 시작하게 되는 문제가 있었습니다. 그래서 프론트엔드 프로젝트를 셋팅하면서 이러한 부분을 고려해야겠단 생각이 들었습니다. 특히 두 가지 부분을 고려하게 되었습니다.

  1. 언제든 디자인이 변경될 가능성이 있다.
  2. 디자인 변경사항을 효과적으로 추적할 수 있어야 하며, 디자인 시스템 구축의 필요성이 커졌다.

이로 인해 프론트엔드에서 기본적으로 UI 컴포넌트를 독립된 환경 에서 문서화 할 수 있는 도구로 자주 쓰이는 Storybook을 도입하게 되었습니다.

이 게시글에서는 Storybook을 설정하고 자동 배포 워크플로우를 구축한 과정을 공유하고자 합니다.

1. Storybook에 대해

Storybook은 컴포넌트의 다양한 경우를 정리할 수 있도록 도와주는 시각화 도구라고 할 수 있습니다. 예를 들어 같은 버튼일지라도 어떤 상태인지, 혹은 각 다른 케이스 별로 이벤트가 달라질 수 있습니다. variant 가 primary면 primary color 버튼 , secondary면 secondary color 버튼이 될 수 있습니다. 이러한 경우를 모두 정리할 수 있도록 도와주는 도구가 Storybook입니다.

특히 디자이너와 협업 할 때 컴포넌트의 UI가 잘 구현되었는지 원활한 소통이 가능하며, 지금처럼 디자이너 없이 시작한 프로젝트에서는 추후 디자이너 온보딩 시 굉장히 중요한 역할을 할 것이라 생각됩니다.

스토리북은 특히 컴포넌트 기반 개발론인 CDD(Component Driven Development) 를 따르고 있을 때 유용한 도구라고 생각합니다. 지금 프로젝트는 CDD를 따르고 있진 않지만 필요한 도구라 생각되어 도입하게 되었습니다.

2. Storybook 설치

먼저 프로젝트에 Storybook을 설치해줍니다. 아래와 같이 설치할 수 있으며 yarn을 사용하고 있기 때문에 yarn을 통해 설치해주었습니다.

yarn add storybook@latest
yarn add storybook-react-router
yarn add -D @storybook/react @storybook/addon-essentials @storybook/addon-interactions

또한 storybook preview.ts에 globals.css를 import 해주어 실제 프로젝트와 동일한 스타일을 적용할 수 있도록 해줍니다.

import '../app/globals.css'

그리고 main.ts에서 어떤 형식이든 stories.tsx 파일이 생성되면 스토리북에서 인식할 수 있도록 수정해주어야 합니다.

stories: [
    "../src/**/*.mdx",
    "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
    "../**/*.stories.mdx",
    "../**/*.stories.@(js|jsx|ts|tsx)",
  ],

이렇게 하면 스토리북에서 어떤 형식이든 stories.tsx 파일이 생성되면 스토리북에서 인식할 수 있도록 해줍니다.

3. Storybook 사용 예시

예를 들어 Button 컴포넌트가 있다고 가정해보겠습니다.

import type { ButtonHTMLAttributes } from 'react'

export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  fullWidth?: boolean
}

const Button = ({
  fullWidth,
  className,
  children,
  color,
  ...rest
}: ButtonProps) => {
  const baseClassName =
    'flex flex-row justify-center items-center gap-2 px-6 py-3 h-14 rounded-2xl font-semibold text-base leading-5 cursor-pointer'
  const widthClassName = fullWidth ? 'w-full' : 'w-[200px]'

  const colorClassName = (() => {
    switch (color) {
      case 'primary':
        return 'bg-[#4CAF50] text-[#FFFFFF]'
      case 'secondary':
        return 'bg-[#E0E0E0] text-[#424242]'
      case 'add':
        return 'bg-[#CF556D] text-[#FFFFFF]'
      default:
        return 'bg-[#4CAF50] text-[#FFFFFF]'
    }
  })()

  const composedClassName = [
    baseClassName,
    widthClassName,
    colorClassName,

    className,
  ]
    .filter(Boolean)
    .join(' ')

  return (
    <button className={composedClassName} {...rest}>
      {children}
    </button>
  )
}

export default Button

위와 같은 버튼 컴포넌트가 있다고 가정할 때 이를 Storybook에서 사용할 수 있도록 같은 폴더에 Button.stories.tsx 파일을 생성해줍니다.

import type { Meta, StoryObj } from '@storybook/react-vite'
import Button from './Button'

const meta: Meta<typeof Button> = {
  title: 'Shared/Button',
  component: Button,
  tags: ['autodocs'],
  parameters: {
    docs: {
      description: {
        component:
          '앱 전반에서 사용되는 기본 버튼 컴포넌트입니다. Primary 스타일로 디자인되어 있으며, 전체 너비나 비활성화 상태를 지원합니다.',
      },
    },
  },
  argTypes: {
    children: {
      description: '버튼에 표시될 텍스트',
      control: { type: 'text' },
    },
    onClick: {
      description: '버튼 클릭 핸들러',
      action: 'clicked',
    },
    fullWidth: {
      description: '버튼을 전체 너비로 표시할지 여부',
      control: { type: 'boolean' },
    },
    disabled: {
      description: '버튼 비활성화 여부',
      control: { type: 'boolean' },
    },
    className: {
      description: '추가할 CSS 클래스명',
      control: { type: 'text' },
    },
  },
}

export default meta
type Story = StoryObj<typeof Button>

export const Primary: Story = {
  args: { children: '확인' },
}

export const Secondary: Story = {
  args: { children: '취소', color: 'secondary' },
}

export const Add: Story = {
  args: { children: '할 일 목록에 추가', color: 'add' },
}

export const FullWidth: Story = {
  args: { children: '확인', fullWidth: true },
}

좀 더 각 항목별로 설명해보자면 아래와 같습니다.

3.1 Meta

Meta는 Storybook에서 컴포넌트의 메타데이터를 정의하는 객체입니다. 이 설정을 통해 스토리북이 컴포넌트를 어떻게 표시하고 문서화 할지 결정합니다.

3.2 Story 정의

Story는 컴포넌트의 특정 상태나 케이스를 정의합니다. 각 Story는 컴포넌트가 어떻게 사용될 수 있는지 보여주는 예시입니다. 위와 같이 Primary , Secondary 처럼 특정 케이스를 정의할 수 있습니다.

이렇게 하면 스토리북에서 각 케이스를 확인하고 특히 Controls 패널에서 실시간으로 props를 변경하며 상태 및 UI 변화를 확인할 수 있습니다.

image

4. Storybook 배포

먼저 Storybook을 배포하기 전에 빌드 스크립트를 추가할 수 있습니다. 스토리북을 설치할 때 알아서 설정되어있지만 그렇지 않다면 package.json 파일에 아래 명령어를 추가해줍니다.

scripts: {
    "storybook" : "storybook dev -p 6006"
}

이를 사용하기 위해선 아래와 같이 입력해줍니다.

yarn storybook

또한 Storybook을 배포할 때는 chromatic 을 사용하여 배포합니다.

chromatic 은 스토리북 관리자가 만든 무료 배포 서비스입니다. 이를 통해 클라우드에 안전하게 배포하고 호스팅 할 수 있습니다.

먼저 chromatic 을 설치해줍니다.

yarn add -D chromatic

패키지 설치 후 깃허브 계정으로 chromatic 에 로그인 해줍니다. 그리고 거기서 새로운 프로젝트를 만들고 깃허브 저장소와 동기화합니다.

이렇게 하면 chromatic 에서 생성된 고유한 project-token을 발급받을 수 있습니다. 그 후 스토리북을 빌드하고 배포하기 위해 package.json 파일에 아래 명령어를 추가해주어야 합니다.

"chromatic": "npx chromatic --project-token=YOUR_PROJECT_TOKEN"

저는 이를 자동 배포로 구축하기 위해 Github Actions 를 사용하여 Storybook-Deploy.yaml 파일을 만들어주었습니다. 개발용 배포 서버와 싱크가 맞추는 dev branch에 PR이 올라갔을 때 UI 컴포넌트 테스트 후 머지하게 하기 위해 pull request workflow를 사용하였습니다. Github secrets에 아까 받은 PROJECT_TOKEN 을 넣어줍니다.

name: Storybook Deployment - Dev
on:
  pull_request:
    branches:
      - dev

concurrency:
  group: chromatic-dev-${{ github.ref }}
  cancel-in-progress: true

jobs:
  chromatic:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: yarn

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Publish to Chromatic
        run: yarn chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }} --exit-once-uploaded
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

아래처럼 배포가 잘 된 것을 확인할 수 있습니다.

image

5. 마치며

위처럼 Storybook을 설정하면서 많은 장점을 얻을 수 있었습니다. 첫 번째로 문서화를 하며 UI 컴포넌트를 팀원들끼리 공유하며 원활한 의사소통을 할 수 있었습니다. 일일이 화면을 보여주는 것이 아닌 해당 컴포넌트를 통해 어떤 상태인지 쉽게 확인할 수 있었습니다.

두 번째로 예기치 못한 컴포넌트 변경점을 감지할 수 있었습니다. 예를 들어 모바일 뷰에만 신경쓰다보니 실제 dev로 PR 올린 후 배포된 storybook을 보고 데스크탑뷰에서 캘린더나 버튼 등의 너비나 간격 등이 이상하게 설정되어 있는 점을 발견할 수 있었습니다. 이처럼 개발자에게 한 번 더 안전장치를 추가해주면서 의도하지 않은 컴포넌트 디자인 및 CSS를 조기에 발견 후 처리할 수 있었습니다.

결론적으로 초기에 설정하는 것에 대한 어려움과 그리고 컴포넌트 마다 stories 파일을 설정해주는 번거로움은 있지만 이를 통해 굉장히 큰 장점 등을 가져오고 프론트엔드 개발팀의 효율성을 향상 시킬 수 있었습니다. 추후 기회가 된다면 컴포넌트 테스트도 좀 더 구체적으로 설정해보며 안정된 배포 환경을 구축하는 것을 목표로 하고 싶습니다.