StoryBook
Storybook?
FE작업에서 API 연결, status 관리 등의 작업도 있지만 가장 중요한 작업은 UI/UX 작업이라고 생각합니다. 제가 개발 분야에서 FE를 선택한 이유 또한 구현한 UI를 통해서 직관적으로 작업 결과를 확인하고 실시간으로 피드백을 할 수 있다는 점에 있습니다. 하지만 이러한 장점이 단점이 되기도 합니다. 테스트 코드를 작성하기 어려운 점, 개별 컴포넌트의 UI를 확인하기 어려운 점, 디자이너에게 배포 이전에 구현된 UI를 공유하기도 어렵습니다. 저는 이러한 문제를 stoybook이 해결해준다고 느꼈습니다. 스토리북의 장점을 꼽아보자면, 다음과 같습니다.
- 컴포넌트 기반 개발을 촉진: Storybook은 개발자가 격리된 환경에서 UI 컴포넌트를 개별적으로 개발하고 테스트할 수 있게 해줍니다.
- 재사용 가능한 컴포넌트 라이브러리 생성: 개발된 컴포넌트를 쉽게 찾아볼 수 있게 하여 재사용성을 높이고, 컴포넌트 라이브러리를 구축하는 데 도움을 줍니다.
- 디자인과 일관성 유지: 디자이너와 개발자 간의 협업을 용이하게 하며, UI의 일관성과 품질을 유지하는 데 기여합니다.
- 문서화: 컴포넌트의 사용 방법, props, 상호 작용 등을 문서화하여 개발 팀 내외부의 커뮤니케이션을 개선합니다.
- 비주얼 테스팅: 컴포넌트의 비주얼 테스트를 자동화하여 UI가 예상대로 렌더링되는지 확인할 수 있습니다.
- 플러그인과 통합: 다양한 플러그인과의 통합을 지원하여, 접근성 검사, 소스 코드 미리보기, 국제화 등의 기능을 추가할 수 있습니다.
- 개발 환경의 개선: 컴포넌트를 실제 애플리케이션과 분리하여 개발함으로써, 복잡한 의존성과 상태 관리 없이 UI를 집중적으로 개발할 수 있습니다.
- 효율적인 협업: 스토리북을 통해 디자이너, 개발자, QA 등 팀원 모두가 UI 컴포넌트에 대해 논의하고, 피드백을 공유하며, 협업할 수 있는 중앙 집중화된 플랫폼을 제공합니다.
storybook 설치
storybook의 설치와 기본 적용 방법은 공식 문서에서 상세하게 안내해주고 있습니다. 저는 Next.js 14.1.4 버전과 storybook 8.0.4 버전을 사용했습니다. (링크) 저는 Next.js 뿐만 아니라 css 방법론으로 Utility-First CSS를 적용했습니다. 이를 위해 tailwind css를 추가로 설치했습니다. storybook에 tailwind를 적용하는 방법 또한 공식문서에서 친절하게 안내하고 있습니다. (링크) 해당 설치 과정을 마치고 나면 다음과 같이 Next project에 storybook이 설치되는 것을 확인할 수 있습니다. 설치 경로의 최상위 루트에 .storybook 디렉토리와 src dir 내부에 stories 디렉토리가 생성된 것을 확인할 수 있습니다.

-
.storybook/main.ts
전체적인 storybook의 설정을 작성합니다. 사용 중인 framework, stories 경로, addons 등을 모두 관리합니다. 사용 중인 framework, stories의 경로는 반드시 작성해야 합니다. 다양한 환경 설정의 방법에 대해서는 여기서 확인할 수 있습니다. (링크) -
storybook/preview.ts
stoybook에 render 되는 컴포넌트 전체에 decorator, parameter를 일괄적으로 적용할 수 있습니다. (링크)-
decorator
import type { Preview } from '@storybook/react'; import React from 'react'; import '../src/app/globals.css'; const preview: Preview = { decorators: [ Story => ( <div style={{ backgroundColor: 'wheat', padding: 20 }}> <Story /> </div> ), ], parameters: { controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, }, }; export default preview;
-
parameter
import type { Preview } from '@storybook/react'; import '../src/app/globals.css'; const preview: Preview = { parameters: { controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, backgrounds: { values: [ { name: 'light', value: '#fff' }, { name: 'dark', value: '#333' }, { name: 'red', value: 'red' }, ], }, }, }; export default preview;
-
Storybook 적용하기
공통 컴포넌트 Button을 다음과 같이 만들었습니다.
'use client';
import clsx from 'clsx';
import { ReactNode } from 'react';
import { HTMLMotionProps, motion } from 'framer-motion';
type ButtonVariant = 'disabled' | 'active' | 'transparent';
type Position = 'fixed' | 'relative';
export interface ButtonProps extends HTMLMotionProps<'button'> {
children: ReactNode;
variant?: ButtonVariant;
position?: Position;
type?: 'button' | 'submit' | 'reset';
isShadow?: boolean;
}
export default function Button({
children,
isShadow,
position = 'relative',
type = 'button',
variant = 'active',
...props
}: ButtonProps) {
const tap = {
scale: 0.98,
transition: { duration: 0.3 },
};
return (
<motion.button
className={clsx(
'w-full max-w-60 min-w-60 h-16 py-2.5 rounded-xl text-lg font-bold cursor-pointer bg-[#6552FE] transition ease-in-out duration-150',
variant === 'disabled' &&
'bg-[#D9D9D9] cursor-not-allowed text-[#718096]',
variant === 'transparent' && 'bg-transparent',
position === 'fixed' && 'fixed bottom-2.5',
isShadow && 'shadow-btnShadow',
props.className,
)}
type={type}
disabled={variant === 'disabled'}
whileTap={variant === 'active' ? tap : undefined}
{...props}>
{children}
</motion.button>
);
}
해당 공통 컴포넌트를 storybook에 렌더링 시키고자 합니다.
storybook의 파일 이름 작성 방법은 다음과 같은 방식으로 작성이 됩니다. 파일이름.stories.js | ts | jsx | tsx
storybook이 인식 가능한 확장자는 main.ts
의 config.stories
옵션에서 정의할 수 있습니다. 아래와 같이 해당 Button component를 storybook에 반영했습니다.
// CustomButton.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import Button from '../app/_component/Button';
const meta: Meta<typeof Button> = {
title: 'Custom Button',
component: Button,
argTypes: {
children: { control: 'text' },
},
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
args: { onClick: fn() },
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Active: Story = {
args: {
children: 'Active',
variant: 'active',
},
};
export const Transparent: Story = {
args: {
children: 'Test',
variant: 'transparent',
},
};
export const Disabled: Story = {
args: {
children: 'Test',
variant: 'disabled',
},
};
CSF(Component Story Format)
Storybook의 Component Story Format (CSF)는 모던 JavaScript로 구성된 구성 요소 스토리를 작성하기 위한 추천 포맷입니다. CSF는 개발자들이 기존의 모듈 시스템을 사용하여 스토리를 작성할 수 있게 하여, 코드의 재사용성과 유지 관리를 용이하게 합니다. 또한, CSF는 스토리북의 강력한 기능들과 자연스럽게 통합되며, 자동 문서 생성, 스토리 소스 보기, 스토리 분류 등을 지원합니다.
CSF에서는 기본적으로 ES 모듈 문법을 사용하여 스토리를 정의합니다. 각 스토리는 React 컴포넌트의 각 변형을 나타내며, export
를 사용하여 스토리를 내보냅니다. 스토리북은 이러한 export된 함수들을 스토리로 인식합니다.
Meta, StoryObj type
Storybook에서 TypeScript를 사용할 때, Meta
와 StoryObj
타입은 컴포넌트 스토리의 정의와 각 스토리 인스턴스를 타입화하는 데 중요한 역할을 합니다. 이를 통해 TypeScript의 정적 타이핑 기능을 활용하여 스토리 정의의 정확성을 보장하고, 자동 완성과 같은 IDE 기능을 향상시킬 수 있습니다.
Meta
타입은 스토리 파일의 메타 데이터를 정의할 때 사용됩니다. 이 메타 데이터에는 스토리북 내에서 해당 컴포넌트가 어떻게 나타날지에 대한 정보(예: 컴포넌트의 타이틀, 분류, 관련된 애드온 설정 등)가 포함됩니다.Meta
타입을 사용하면 이러한 메타 데이터를 정의할 때 타입 안정성을 제공합니다.StoryObj
타입은 개별 스토리를 정의할 때 사용됩니다. 각 스토리는 컴포넌트의 특정 상태를 나타내며,StoryObj
타입을 사용하여 이러한 스토리가 올바른 형식으로 정의되었는지 확인할 수 있습니다.StoryObj
타입을 사용하면 스토리 정의에args
와 같은 필드가 올바르게 포함되었는지, 그리고args
가 컴포넌트의 props 타입과 일치하는지 확인할 수 있습니다. 이를 통해 스토리북에서 스토리를 정의하고 관리하는 과정에서 타입 안정성을 보장할 수 있습니다.
ArgTypes
Storybook의 **argsTypes
**는 스토리북 UI에 컨트롤(controls)을 추가하여, 개발자나 디자이너가 컴포넌트의 props를 동적으로 조정할 수 있게 해주는 중요한 속성입니다. 이를 통해 컴포넌트의 다양한 상태를 빠르게 시각화하고 테스트할 수 있습니다.
**argsTypes
**는 스토리의 메타 데이터 부분에 정의되며, 각각의 **argsType
**은 해당 컴포넌트 prop의 이름을 키로 사용하고, 이 prop의 제어 방법을 설명하는 객체를 값으로 가집니다. 이 객체 내에서 타입(type), 설명(description), 기본값(defaultValue), 그리고 제어할 수 있는 방식(control) 등의 정보를 설정할 수 있습니다. (링크)
parameters
Storybook의 parameters
API는 스토리 또는 스토리북 전체에 대한 메타 데이터를 설정하는 방법을 제공합니다. 이를 통해 스토리의 렌더링 방식, 스토리북의 애드온 동작, 스토리북 UI의 특정 부분의 보임/안 보임 등을 세밀하게 제어할 수 있습니다. **parameters
**는 스토리의 전역 설정에 적용될 수도 있고, 개별 스토리에 대해 설정될 수도 있습니다.
docs
각 스토리북에 렌더링하는 컴포넌트를 문서화 하여 정리할 수 있습니다. 컴포넌트 파일에서 JSDOCS를 활용해서 컴포넌트에 관한 설명, 각 prop의 설명을 추가할 수 있으며, tags: [”autodocs”]
설정을 통해서 전체 스토리북 코드를 문서화 할 수 있습니다.
export interface ButtonProps extends HTMLMotionProps<'button'> {
/**버튼 컴포넌트 내부 텍스트 */
children: ReactNode;
/** 버튼 활성화 여부 */
variant?: ButtonVariant;
/** 버튼 위치*/
position?: Position;
/** 버튼의 용도 */
type?: 'button' | 'submit' | 'reset';
/** 그림자 여부 */
isShadow?: boolean;
}
/** 버튼 컴포넌트에 대해서 소개합니다 */
export default function Button({
children,
isShadow,
/....
){}

배포하기
해당 배포 방식은 storybook 공식 문서의 배포 하기를 참고했습니다. (링크)
Chromatic을 통해서 쉽게 storybook을 배포 할 수 있습니다. Chromatic은 스토리북(Storybook) 프로젝트를 위한 자동화된 시각적 테스팅 서비스입니다. 스토리북을 사용하여 UI 컴포넌트를 개발하고 문서화하는 과정에서, Chromatic을 통해 컴포넌트의 시각적 변화를 추적하고 검토할 수 있습니다. 이 서비스는 컴포넌트가 변경될 때마다 스크린샷을 캡쳐하고 이전 버전과 비교하여 변화를 감지합니다. 만약 차이가 발견되면, 변경 사항을 검토하고 승인할 수 있는 워크플로우를 제공합니다.
github action 추가하기
git repository에 secrets code를 추가합니다. 해당 secrets 코드는 chromatic에서 발급한 project-token 값을 입력합니다.

이후 /.github/workflow
디렉토리에 chromatic.yml
폴더를 아래와 같이 작성합니다.
name: 'Chromatic Deployment'
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: yarn
- uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
해당 secrets에서 GITHUB_TOKEN을 설정할 필요가 없는 이유는 GitHub Actions가 실행될 때 GitHub에 의해 자동으로 생성되고 주입되기 때문입니다. 이 토큰은 워크플로우 내에서 GitHub API에 안전하게 액세스하기 위해 사용됩니다.
장점
- 배포가 쉽습니다. chromatic 연결, 명령어 몇 줄 입력으로 빠르게 배포를 마칠 수 있습니다. 또한 피그마와 연동하여 피그마에 쉽게 해당 컴포넌트를 가지고 올 수 있습니다.
단점
- 빌드 시간이 너무 오래 걸립니다. github action의 캐싱 기능을 잘 활용 하지 못해서 일 수도 있지만, 그렇게 크지 않은 스토리북 배포에도 약 3분의 시간이 소요되었스빈다.
- chromatic을 활용한다면, UI test에 횟수 제한이 있습니다.
- Storybook?
- - storybook 설치
- Storybook 적용하기
- - CSF(Component Story Format)
- - Meta, StoryObj type
- - ArgTypes
- - parameters
- - docs
- 배포하기
- - github action 추가하기
- - 장점
- - 단점