
프로젝트를 시작하기 전, Vue 기반 작업이 위주라 React는 실무가 아닌 연습만 하고 있었는데, 계속 눈에 익히지 않으면 잊어버리는 터라 관련 프로젝트를 한번 해봐야겠다고 결심했다. 그때 문득, 1년 전쯤 했던 사이드 프로젝트 중 하나인 Storybook 제작이 떠올랐다. 당시엔 프로젝트를 시작하기 전에 잠깐 작업을 해본 정도였고, 그 당시에는 많은 것을 몰라 어영부영 진행되었다.
이번에는 1년 전보다는 훨씬 더 많은 지식과 경험을 쌓았고, React 환경에서 Storybook을 활용한 공통 UI 제작이라는 목표를 설정하면 이해도가 높을 것 같았다. 그래서 이 프로젝트를 다시 시작하기로 했다. 처음부터 환경을 구축하고, 프로젝트에서 자주 사용됐던 공통 컴포넌트를 만들어 볼 계획이다.
이 프로젝트를 통해, React와 Storybook을 실전에서 활용해 보며 많은 것들을 배울 수 있을 것이라 기대한다. 이번에도 기한이 없이 프로젝트 들어가기 전까지로 기간은 잡혀있지만, 오래 걸리더라도 완성해보려 한다!
npm create vite@latest [프로젝트명] --template react
React + TypeScript를 선택해주었다.

스토리북 공식 설치 방법을 참고했다.
npx storybook@latest init

설치완료! 이제 pnpm run storybook를 통해 스토리북을 실행할 수 있다.

.stroybook, src/stories/ 폴더가 생성된걸 확인할 수 있다.
css import를 편하게 하기 위해 파일명 앞에 src 대신 @/가 오도록 설정해보자.
@types/node 설치를 하면 된다.
pnpm add -D @types/node
//vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import * as path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
})
조건부 클래스를 사용하려니 삼항연산자조건 ? 참일 때 값 : 거짓일 때 값로만 사용해야해서 불편하다. 깔끔하게 관리할 수 있도록 도와주는 라이브러리를 설치했다.
src/gloal.d.ts 파일 생성import { clsx } from 'clsx';
ㅤ
declare global {
// globalThis에 clsx 속성을 추가
interface GlobalThis {
clsx: typeof clsx;
}
}
ㅤ
export {};
src/main.tsx, preview.ts(Storybook) 설정 추가import { clsx } from 'clsx';
ㅤ
globalThis.clsx = clsx;
스토리북을 설치하면 기본적으로 @/stories 폴더 아래에 모든 컴포넌트 관련 파일이 포함된다. 하지만 작업을 진행해보니, 개별 컴포넌트와 해당 스토리 파일을 하나의 폴더로 묶어서 관리하는 방식이 훨씬 효율적이었다.

이렇게 하면 컴포넌트와 관련된 파일을 한눈에 확인할 수 있고, 유지보수도 쉬워진다.
원래 프로젝트에서의 스타일 적용은 이렇다.

@/styles/components 폴더에 UI별로 스타일 파일을 따로 생성main.scss에서 @use로 선언App.tsx에서 main.scss import스토리북은 App.tsx와 관련이 없으므로 위와 같이 적용하면 css가 적용되지 않는다. 스토리북에서도 이렇게 전역 선언하는 파일이 있는데 .storybook/preview.ts 이다. 이 파일에 import 해주자!
preview.ts ?
글로벌 스타일 설정, 라이브러리 초기화 또는 구성 요소를 렌더링하는 데 필요한 다른 모든 작업에 사용한다. 해당 파일에 적용하는 내용은 구성 요소가 렌더링되는 미리보기 iframe 에 삽입하며 , Storybook 애플리케이션 UI에는 삽입하지 않는다. Storybook 공식 문서// .stories/preivew.ts import type { Preview } from "@storybook/react"; import '@/styles/main.scss' ㅤ const preview: Preview = { parameters: { controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, }, }; ㅤ export default preview;
☝️ 스타일 파일을 한 곳에 모을 필요가 없다면, Story 컴포넌트 폴더에 스타일 파일을 넣는 방식도 좋아보인다.
/stories
/Button
- index.tsx
- Button.stories.tsx
- button.scss
스토리북에서 보이는 화면을 세팅하는 파일이다. 간단하게 사용하면 어렵지 않다!
간단하게 정리해보았다. 공식문서를 참고하자!
import { fn } from '@storybook/test';
import { CmButton } from './index';
ㅤ
const meta = {
title: 'Example/CmButton', // 폴더 구조 (depth)
component: CmButton, // import한 컴포넌트명
parameters: {
layout: 'centered', // 컴포넌트가 스토리북에서 보여지는 방식
},
tags: ['autodocs'], // 자동 Docs 생성
argTypes: { // 컨트롤 방식 및 허용하는 값 설정, 설명
backgroundColor: { control: 'color' },
},
args: { onClick: fn() }, // 스토리북에서 볼때 Default값 지정
} satisfies Meta<typeof CmButton>;
// `title`에서 생성한 폴더 하단으로 아래 export한 속성을 가진 페이지가 생성된다!
export const Basic: Story = {
args: {
variant: 'primary',
children: 'Button',
},
};

이 방법 외에도 복잡한걸 구현할 수도 있는데, 나는 CmButton 클릭했을때 CmModal이 나오는 화면을 보여주려고 ReactHooks로 작업해보았다!
// CmModal.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import React, { useState } from 'react';
import { CmButton } from '../CmButton/index';
import { CmModal } from './index';
const meta = {
title: 'Example/CmModal',
component: CmModal,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
},
args: {
onOpenClick: (open: boolean) => {console.log('Modal opened:', open);}
},
} satisfies Meta<typeof CmModal>;
export default meta;
type Story = StoryObj<typeof CmModal>;
const ModalWithButton = (
{ noFooter }: { noFooter?: boolean;) => {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<>
{/* CmButton을 눌렀을 때 모달 열기 */}
<CmButton variant="primary" onClick={handleOpen}>
모달 열기
</CmButton>
<CmModal
title="모달 제목"
open={open}
onOpenClick={setOpen}
noFooter={noFooter}
confirm={handleClose}
>
<div>모달 내용</div>
</CmModal>
</>
);
};
export const Basic: Story = {
render: () => <ModalWithButton />,
};
export const noFooter: Story = {
render: () => <ModalWithButton noFooter />,
};
1. 예시 페이지 없이 UI 개발 가능
스토리북을 사용하면서 가장 큰 장점 중 하나는 매번 프로젝트마다 예시 페이지를 따로 만들 필요가 없다는 점이다. 컴포넌트마다 예시 페이지를 생성하는 시간도 무시할 수 없는데, 스토리북에서도 컴포넌트명.stories.ts 파일이 있지만 적응만 한다면 이전보다 빠르게 적용할 수 있을 것 같다.
2. 자동으로 추가되는 옵션들
새로운 상태나 속성을 추가할 때마다 UI에서 자동으로 그 변경 사항을 반영해주어, 예시 페이지를 수정하지 않아도 된다.
3. 조건에 따른 UI 변화 확인 용이
스토리북에서는 컴포넌트의 상태나 조건을 설정하면, 그에 따른 UI 변화를 쉽게 확인할 수 있다. 기존에는 페이지 내에서 상태를 변경하며 확인해야 했지만, 스토리북은 조건에 맞는 설정이 생성되기 때문에 별도로 변경없이 상태에 따른 UI를 바로 확인할 수 있다.
4. 독립적인 개발 환경
스토리북은 독립적인 개발 서버에서 실행되기 때문에, 사용자 프로젝트에서 예시 폴더나 예시 페이지를 따로 관리할 필요가 없다.
1. 번거로운 초기설정
스토리북을 처음 설정할 때, 사용자 프로젝트와 동일하게 환경을 구축하는 데 시간이 걸린다. 특히, TypeScript나 특정 라이브러리를 사용할 경우 설정이 조금 복잡해진다.
2. 성능 저하 및 관리 문제
스토리북에서 컴포넌트의 수가 많아지거나 각 컴포넌트의 props가 복잡해지면 렌더링 속도가 느려질 수 있다. 특히, 상태 관리나 외부 API와의 연동이 필요한 컴포넌트의 경우 실제 애플리케이션 환경과 다르게 동작하거나 추가적인 문제가 발생할 수 있다. 이러한 문제를 해결하려면 스토리북을 초기 설정 이후에도 계속 관리해야 하는 번거로움이 생긴다.
이번 프로젝트에서는 "리액트와 친해지길 바래"라는 목표를 위해 스타일은 기존 Vue에서 사용하는 방법을 그대로 적용했다. 스타일링 방식까지 고민하면 리액트에 익숙해지는 데 시간이 더 걸릴 것 같아 스타일은 신경 쓰지 않고 리액트 사용에 집중했다.
스토리북을 사용하면서 가장 큰 장점은 새로운 상태나 속성이 추가될 때마다 UI가 자동으로 반영되어 예시 페이지를 따로 수정하거나 관리할 필요가 없다는 점이었다. 하지만 예시 페이지를 만들 때 소요되는 시간만큼 스토리북 설정에도 시간이 걸렸다. 프로젝트와 동일한 환경을 구축하는 데 시간이 소요되었고, 복잡한 컴포넌트를 사용하게 될 경우 초기 설정 이후에도 관리가 필요했다.
초기 세팅이 조금 번거롭긴 했지만, 장기적으로 효율성면에서 좋아보이고 팀에서 UI 문서를 작성할 때도 유용하게 활용할 수 있을 것 같다. 각자의 프로젝트나 팀 환경에 맞춰 장단점을 고려한 후, 원하는 방식으로 활용하면 좋을 것 같다.
참고
스토리북