본문으로 건너뛰기

스토리 파일을 뜯어보며 스토리북의 스토리 이해하기

🎯 이 문서를 읽고 난 후의 상태

  • 스토리북에서 말하는 스토리가 무엇인지 안다.
  • 스토리의 구조를 안다.

🤔 가이드 문서와는 조금 다른 접근

스토리북 가이드가 정말 잘 작성되어 있는 것은 맞다.

하지만 그냥 따라가기만 해서는 재미가 없다고 생각한다.

그리고 나같이 미숙한 사람에게는 이해가 쉽지 않기도 하고 말이다.

그래서 템플릿 코드에 있는 스토리를 한번 분석해보면서 스토리를 이해하고자 한다.

📝 디렉토리 구조 파악

스토리북 템플릿의 디렉토리 구조
스토리북 템플릿의 디렉토리 구조

디렉토리를 열어보니 다음과 같은 디렉토리 구조를 갖고 있었다.

아무래도, stories는 스토리북의 템플릿 데이터를 모아놓은 곳 같았다.

찾아보니 .css.jsx 등은 어디까지나 템플릿 구성을 위한 것이고, 실상은 Button.stories.js와 같은 .stories. 파일이 중요하다고 한다.

그리고, 스토리북이 알아서 .stories. 파일을 찾아서 보여주기에, 이게 어디에 있든 상관은 없다고 한다.

다만 추후 배포나, 관리의 측면에서 스토리들은 굳이 배포할 필요가 없기에, 따로 모아놓는 것이다.

이게 관리가 더 편하기도 하고..

🤔 스토리란 무엇인가?

본격적으로 스토리를 분석하기 전에, 스토리가 무엇인지부터 이해하고 넘어가야할 듯 하다.

A story captures the rendered state of a UI component.

스토리북 공식 문서에서는 스토리를 위와 같이 정의하고 있다.

이와 관련해서 GPT에게 물어봤을 때 다음과 같은 답변을 받을 수 있었다.

🤖 GPT의 답변

Storybook에서 스토리는 특정 컴포넌트의 단일 상태를 설명하는 예제입니다. 스토리는 사용자가 개별 UI 컴포넌트의 다양한 상태를 시각적으로 확인하고, 테스트하며, 문서화할 수 있도록 합니다. 스토리는 각 컴포넌트가 다양한 props를 통해 어떻게 동작하고 렌더링되는지 보여주는 일종의 "샘플"이라고 볼 수 있습니다.

스토리의 기본 개념

  • 독립적인 예제: 스토리는 컴포넌트를 독립적으로 렌더링하여 특정 상태나 props 조합에서 컴포넌트가 어떻게 보이고 동작하는지를 확인할 수 있습니다.
  • 테스트 및 문서화: Storybook은 스토리를 기반으로 컴포넌트의 다양한 상태를 문서화하고 UI 테스트를 진행할 수 있는 환경을 제공합니다.
  • 인터랙션: 사용자는 Storybook의 컨트롤 패널을 통해 props를 변경하고, 다양한 상태에서 컴포넌트가 어떻게 변하는지를 실시간으로 확인할 수 있습니다.

스토리의 구성 방식

스토리는 .stories.js, .stories.jsx, .stories.ts, .stories.tsx 등의 파일 내에서 정의됩니다. 각 스토리는 컴포넌트를 특정 상태로 설정하여 렌더링하는 함수나 객체로 작성됩니다.

스토리의 기본 예시

javascript
// Button.stories.js
import React from 'react';
import { Button } from './Button';

export default {
title: 'Button', // Storybook의 탐색 트리에 표시되는 제목
component: Button, // 렌더링할 컴포넌트
};

export const Primary = () => <Button label="Primary Button" primary />;
export const Secondary = () => <Button label="Secondary Button" />;

위 코드에서 PrimarySecondary는 각각 Button 컴포넌트의 스토리입니다. Storybook에서는 이를 개별적으로 확인할 수 있으며, 각기 다른 props가 적용된 상태를 시각적으로 보여줍니다.

스토리의 특징

  1. 컴포넌트 상태 설명: 스토리는 컴포넌트가 다양한 props와 상태에서 어떻게 보이고 동작하는지를 명확하게 설명합니다.
  2. 독립성: 각 스토리는 독립적으로 동작하므로 다른 스토리와의 의존성이 없습니다. 이는 컴포넌트의 특정 상태를 격리된 환경에서 테스트할 수 있도록 합니다.
  3. 재사용성: 스토리는 UI 테스트와 문서화에 사용되며, argsargTypes를 사용해 props를 쉽게 변경할 수 있습니다.

스토리 작성 시 유용한 사항

  • 스토리 이름: 각 스토리는 Storybook UI에 표시될 이름을 가지며, 일반적으로 함수 이름이나 객체의 키 이름이 그 역할을 합니다.
  • 스토리 그룹화: 여러 스토리를 작성할 때, 같은 컴포넌트의 스토리를 한 파일에 모아서 작성할 수 있습니다.
  • args 사용: 스토리에서 props를 전달할 때 args를 사용하면 더 직관적으로 다양한 상태를 설정할 수 있습니다.

스토리 예시 (args 사용)

javascript
export const WithArgs = (args) => <Button {...args} />;
WithArgs.args = {
label: 'Button with args',
primary: true,
};

스토리가 중요한 이유

  • 디자인 시스템 및 UI 구성 요소 개발: Storybook을 통해 UI 구성 요소를 다양한 시나리오에서 테스트함으로써 보다 안정적이고 일관성 있는 컴포넌트를 개발할 수 있습니다.
  • 빠른 피드백: 스토리를 통해 컴포넌트의 다양한 상태를 미리 확인하고 피드백을 받을 수 있어 개발 속도를 높이고 버그를 줄일 수 있습니다.
  • 문서화: 스토리 자체가 일종의 문서로 작용하여 팀 내 개발자, 디자이너, 비기술자 모두가 쉽게 컴포넌트를 이해할 수 있습니다.

요약하자면, 스토리는 Storybook에서 개별 컴포넌트의 상태를 설명하고 시각적으로 확인할 수 있는 독립적인 예제입니다. 이를 통해 개발자는 컴포넌트의 다양한 상태를 손쉽게 확인하고 문서화할 수 있습니다.

쉽게 말하면, 예제이다. 해당 컴포넌트가 어떻게 사용되는지를 보여주는 예제.

뒤에 이어서 설명하겠지만, 여기에 관련된 설정들은 예제의 환경을 설정해주거나, 초기값을 잡아주거나 하는 것들이다.

컴포넌트에 대한 문서가 스토리북이다보니, 당연하게도 여기에는 컴포넌트에 들어가는 props 가 존재한다.

props가 어떻게 변하는지에 따라, component의 레이아웃이나 여러 요소들이 달라지게 된다.

스토리에 이런 초기값에 대한 설정들을 넘겨줘서, 예제로써 문서에서 개발자나 다른 사용자들이 쉽게 이 컴포넌트가 어떻게 동작하는지 파악할 수 있게 하는 역할이라고 보면 될 듯 하다.

📝 스토리 파악

본격적으로 딥다이브 하기에 앞서서 스토리 하나를 파해쳐보기로 했다. 바로 어떤 가이드를 따라하는 것보다는 직접 한번 하나하나 살펴보고 들어가면 좋겠다는 생각에서 였다.

스토리북은 기본적으로 컴포넌트 기반 개발(Component-Driven Development)을 기본적을 채택하고 있다.

어렵게 생각할 것 없이 컴포넌트 하나에서 시작해서, 천천히 위로 올라가면서 하나씩 완성하는 개발 방법론이다.

합성 패턴(Composition pattern)으로 살펴보는 리액트 컴포넌트 설계 핵심

내가 기존에 작성했던 글인데 한번 참고해도 좋을 듯 하다.

어쨋든, 이런 관점에서 처음 진입점을 Button으로 잡게 되었다.

제일 작은 단위일테니 파악이 쉽지 않을까 하는 생각에서 였다.

Button.js
import React from 'react';
import PropTypes from 'prop-types';
import './button.css';

/**
* Primary UI component for user interaction
*/
export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={backgroundColor && { backgroundColor }}
{...props}
>
{label}
</button>
);
};

Button.propTypes = {
/**
* Is this the principal call to action on the page?
*/
primary: PropTypes.bool,
/**
* What background color to use
*/
backgroundColor: PropTypes.string,
/**
* How large should the button be?
*/
size: PropTypes.oneOf(['small', 'medium', 'large']),
/**
* Button contents
*/
label: PropTypes.string.isRequired,
/**
* Optional click handler
*/
onClick: PropTypes.func,
};

Button.defaultProps = {
backgroundColor: null,
primary: false,
size: 'medium',
onClick: undefined,
};

버튼의 코드는 위와 같다. 스토리북은 문서이다. 이에 따라서, input/output 등이 명확해야하는 필요가 있다.

이에 대한 좋은 방법은 타입을 지정해주는 것이다.

이에 따라서 위와 같이 별도로 타입을 명세해준 것을 볼 수 있다.

사실 이런 관점에서 개인적으로는 타입스크립트를 통해서 storybook 작성하는 것을 선호하는 편이기도 하다.

Button.stories.js
import { fn } from '@storybook/test';
import { Button } from './Button';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
export default {
title: 'Example/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
};

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};

export const Secondary = {
args: {
label: 'Button',
},
};

export const Large = {
args: {
size: 'large',
label: 'Button',
},
};

export const Small = {
args: {
size: 'small',
label: 'Button',
},
};

그리고 이에 따른 stories는 위와 같다.

Button 컴포넌트는 그렇다고 쳐도, 이를 어떻게 표현하고 있는지를 이해하는 것이 중요해보인다.

그 전에, 이게 스토리북 화면으로는 어떻게 표시되는지 함꼐 알아보자.

스토리북 이미지
스토리북 이미지

위와 같이 표시되는 것을 볼 수 있다.

이에 따른 컴포넌트를 하나씩 살펴보자.

✅ import문

import 문
import { fn } from '@storybook/test';
import { Button } from './Button';

최근의 코드는 위와 같이 사용하는 듯 하다.

스토리북 자체적으로 mocking을 지원하고 있기에, 이를 위해서 fn을 import 해오는 부분이다.

함수에 대한 mocking이 필요없다면, 제외해도 된다.

그리고 우리가 스토리를 작성할 Button 컴포넌트를 import한 모습이다.

✅ title

js
export default {
title: 'Example/Button',

주석은 무시하고 하나씩 살펴보자.

title은 스토리북 사이드바에 어떻게 보여질지를 의미한다.

스토리북 사이드바 화면
스토리북 사이드바 화면

여기에 EXAMPLEButton이 보이는가? 이걸 결정하는게 title 이다.

쉽게 말하면 앞에는 디렉토리, 뒤에는 컴포넌트라고 볼 수 있다.

✅ component

js
  component: Button,

Component는 우리가 스토리북에서 보여줄 컴포넌트이다.

js
import { Button } from './Button';

아까 import한 이 컴포넌트를 의미한다.

✅ parameter

js
  parameters: {
layout: 'centered',
},

parameters는 스토리의 동작을 커스터마이징 하거나, 조정하기 위해서 사용된다.

스토리의 렌더링 방법, 애드온 동작 방식, 특정 환경 설정 등을 제어할 수 있도록 도와주는 객체이다.

parameters의 주요 목적은 스토리별 설정을 Storybook에 제공하여 스토리의 표현이나 동작 방식을 제어하는 것이라고 한다.

예를 들어, 스토리의 레이아웃을 설정하거나 특정 애드온의 동작을 활성화/비활성화하는 등 다양한 옵션을 지정할 수 있다.

여기서는 layout: 'centered'라는 옵션을 사용했다.

이게 무엇이냐면, 버튼 컴포넌트가 보이는 위치의 레이아웃을 정의하는 속성이다.

스토리북 컴포넌트 화면
스토리북 컴포넌트 화면

만약 이 옵션을 다음과 같이 바꾼다면 화면은 다르게 표현된다.

js
  parameters: {
layout: 'left',
},
스토리북 컴포넌트 화면
스토리북 컴포넌트 화면

이런 느낌의 옵션이다. 개인적으로는 layout: 'centered'를 유지하는 것을 추천한다.

✅ tags

js
  tags: ['autodocs'],

tags 속성은 Storybook 7부터 도입된 새로운 기능이다.

스토리를 더 쉽게 분류하고 특정 그룹이나 유형에 따라 필터링할 수 있게 해주는 기능이다.

이 속성은 스토리에 메타데이터를 추가해주는 역할을 하며, 다양한 목적에 따라 스토리를 분류하고 관리하는 데 사용된다.

tags는 각 스토리나 스토리 파일에 추가할 수 있는 문자열 배열로, 특정 키워드나 태그를 부여하여 Storybook의 탐색 및 필터링을 지원하며, 이를 통해 여러 스토리 중에서 특정 속성을 갖는 스토리만을 쉽게 검색할 수 있다.

autodocs 옵션은 자동으로 스토리를 문서에 포함시키는 기능이다.

js
  //tags: ['autodocs'],

만약 tags를 위와 같이 주석처리하면 다음과 같이 에러가 발생한다.

스토리북 메인 화면에서 에러 발생
스토리북 메인 화면에서 에러 발생

세부설정은 나중에 뒤에서 알아보기로 하고, 우선은 해당 옵션을 사용하는 것을 권장한다.

✅ argTypes

여기서부터가 storybook의 핵심이 되는 속성이다. 위에까지는 있는 설정 그대로 사용해도 됐다면, 여기서부터는 코딩이 조금 필요한 구간이다.

나도 지금은 배우는 단계이기에, GPT의 힘을 빌렸다. GPT가 설명한 내용은 다음과 같다.

🤖 GPT의 답변

argTypes는 Storybook에서 스토리의 args(인수)에 대한 추가적인 메타데이터를 정의하는 옵션입니다. 이를 통해 스토리에서 사용할 컴포넌트의 props를 보다 상세히 제어하고 문서화할 수 있습니다. argTypes를 설정하면 Storybook의 Controls 패널에 표시되는 방식이나 각 prop의 행동을 사용자 정의할 수 있습니다.

argTypes의 주요 목적

  1. Controls 패널 커스터마이징: 특정 prop의 표시 방식과 종류를 정의하여 사용자가 Storybook의 UI를 통해 더 직관적으로 컴포넌트를 조작할 수 있습니다.
  2. 자동 문서화: 각 prop의 설명과 기본값을 설정하여 Storybook의 자동 문서화 기능을 개선할 수 있습니다.
  3. 이벤트 핸들링: 이벤트 prop을 action으로 설정하여 스토리에서 발생하는 이벤트를 Storybook의 Actions 애드온으로 캡처할 수 있습니다.

argTypes 사용법

argTypes는 컴포넌트의 각 prop에 대해 속성을 설정할 수 있습니다. 여기에는 control, description, defaultValue 등의 속성이 포함됩니다.

기본 예시
javascript
export default {
title: 'Button',
component: Button,
argTypes: {
label: {
control: 'text', // 텍스트 입력형 컨트롤
description: 'The label text for the button',
defaultValue: 'Click me',
},
onClick: {
action: 'clicked', // 클릭 이벤트를 Storybook의 Actions 애드온에 기록
},
size: {
control: {
type: 'select', // 드롭다운 선택형 컨트롤
options: ['small', 'medium', 'large'],
},
description: 'Sets the size of the button',
},
},
};

주요 옵션 설명

  1. control: args의 입력 방식을 설정합니다. 이를 통해 사용자 인터페이스에서 prop을 어떻게 조작할 수 있는지를 정의합니다.
  • text: 텍스트 입력
  • boolean: 체크박스
  • select: 드롭다운 메뉴
  • color: 색상 선택기
  • 기타 다양한 컨트롤 유형이 있음
  1. description: prop의 설명을 추가하여 자동 문서화 시 유용하게 사용됩니다.
  2. defaultValue: 기본값을 지정할 수 있으며, 이는 스토리의 초기 상태를 설정합니다.
  3. action: 함수형 prop을 지정하여 스토리에서 발생하는 이벤트를 Storybook의 Actions 애드온으로 캡처할 수 있습니다.

구체적인 예시

javascript
export default {
title: 'ToggleSwitch',
component: ToggleSwitch,
argTypes: {
isOn: {
control: 'boolean',
description: 'Determines if the switch is in the "on" position',
defaultValue: false,
},
label: {
control: 'text',
description: 'Label displayed next to the switch',
},
onToggle: {
action: 'toggled', // 이벤트를 기록
},
},
};

export const DefaultSwitch = (args) => <ToggleSwitch {...args} />;

이 예시에서 isOn prop은 boolean 타입으로 컨트롤되며, labeltext 컨트롤을 사용합니다. onToggle은 함수형 prop으로 설정되어 클릭 이벤트가 Storybook의 Actions 패널에 기록됩니다.

argTypes를 사용하면 좋은 경우

  • 사용자 인터페이스 커스터마이징: 컴포넌트의 다양한 prop을 손쉽게 시각적으로 조작할 수 있도록 설정할 때.
  • 문서화 강화: 팀 내에서 컴포넌트의 prop에 대한 명확한 설명을 제공하여 문서화를 개선할 때.
  • 이벤트 디버깅: 이벤트 핸들러를 action으로 설정해 특정 이벤트가 어떻게 발생하는지를 디버깅할 때.

argTypes는 스토리의 args 설정을 정교하게 조정해 Storybook을 통한 컴포넌트 탐색과 사용을 크게 향상시킬 수 있습니다.

위의 내용을 요약하면 다음과 같다.

argTypes는 스토리의 인수에 대한 추가적인 옵션을 정리한다.

쉽게 생각해보면, 리액트에서 쓰이는 props를 상세하게 정의하고 설명하기 위한 옵션이라고 보면 된다.

스토리북 컴포넌트의 props 조절 화면
스토리북 컴포넌트의 props 조절 화면

위의 화면에 대한 설정을 하는 부분이 argTypes라고 보면 될 듯 하다.

위의 요소를 보면 props 관련된 설명이 굉장히 자세하게 적혀있는 것을 볼 수 있다.

props의 각 속성에 대해서 정의할 수도 있지만, action에 대해서 정의할 수도 있다.

액션이라고 말하니 어려운 말 같은데, 쉽게 생각하면 함수이다. 더 쉽게 생각하면 onClick과 같은 이벤트이다.

사용자가 특정 행동(Action)을 하였을 때 그때의 과정을 중간에서 살펴보기 위한 설정이라고 보면 된다.

Control의 경우는 HTML 태그의 <from> 태그에서 사용되는 <input> 태그를 떠올려보면 이해가 쉬울 듯 하다.

input 태그에도 여러 옵션을 줄 수 있는데, text, radio, checkbox 등의 옵션을 생각해보면 좋을 듯 하다.

아직까지 내가 이해한 바로는, 유사한거 같기도...?

✅ args

js
  args: { onClick: fn() },

args 옵션은 props에 전달되는 값이다.

좀 더 자세히 설명하면, 스토리북에서 각 스토리에 기본적으로 전달될 default 속성 값을 정의할 때 사용된다.

당연하게도 default 값인 만큼 각 스토리에서 컴포넌트가 랜더링 될 때 초기 상태로 적용된다.

Button.jsx
export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
style={backgroundColor && { backgroundColor }}
{...props}
>
{label}
</button>
);
};

Button.propTypes = {
primary: PropTypes.bool,
backgroundColor: PropTypes.string,
size: PropTypes.oneOf(['small', 'medium', 'large']),
label: PropTypes.string.isRequired,
onClick: PropTypes.func,
};

위에서는 Button 컴포넌트를 보면 onClick 이벤트를 외부에서 받아서 주입할 수 있도록 되어 있다. (Button.propTypes 에서 확인 가능)

이에 따라서, 초기 값으로 onClick 이벤트를 정의해줄 필요가 있어서 args: {onClick: fn()}으로 정한 듯 하다.

추가적으로 onClick 이벤트 핸들러를 삽입하기 전에, 그냥 기본 동작으로 아무것도 안함을 설정하기 위해 mocking을 한 것 같기도 하다.

✅ story

마지막으로 제일 중요한 스토리이다. 앞에서 이미 우리는 스토리에 대해 살펴본 적이 있다.

스토리란 무엇인가?

js
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};

export const Secondary = {
args: {
label: 'Button',
},
};

export const Large = {
args: {
size: 'large',
label: 'Button',
},
};

export const Small = {
args: {
size: 'small',
label: 'Button',
},
};

코드의 마지막은 스토리를 정의하고 있는 부분이다.

스토리북 컴포넌트의 props 조절 화면
스토리북 컴포넌트의 props 조절 화면

이렇게 작성한 스토리들은 위와 같은 화면으로 따로 Stories로 구분되어서 출력이 된다.

그리고 이들은 사이드바에서 선택해서 각각의 스토리의 경우를 따로 빼서 볼 수도 있다.

스토리북 사이드바 화면
스토리북 사이드바 화면

위의 Primary, Secondary 등이 바로 그 화면이다.

코드를 보면 알겠지만 Buttonprops에 들어갈 값을 정의하고 있다.

이는 앞서 설명했던 args와 유사하게 사용된다고 봐도 될 듯 하다.

export default 해서 내보내는 부분은 초기값이라고 보면 되고, 밑에 따로 설정하는 부분은 진짜 스토리라고 봐도 될 듯 하다.

🚀 정리

  • 스토리는 스토리북에서 컴포넌트가 어떻게 활용되는지를 보여주기 위한 예제이다.
  • .stories. 파일은 예제가 어떻게 보여질지, default 값은 어떻게 설정할 지와, 추가적인 예제(story)로 구성된다.
  • 무언가 복잡해보이지만, 하나씩 뜯어보면 생각보다 별거 없다.

이렇게 정리가 될 듯하다.

다른 컴포넌트에 대한 스토리를 살펴보아도, 이정도 내용이면 이제는 옵션 값에 대해서 이해를 하는 것 정도만 남은 듯 하다.

이는 하나하나 찾아가면서 달달 외우고 쓰기 보다는, 직접 해보면서 그때그때 필요한 옵션을 찾아 사용하는게 맞는 것 같다.

찾아보면서 주변에 어떤 옵션이 있는지도 좀 보고 말이다.

지금까지는 내가 어떻게 스토리를 이해했는지를 서술했다면, 이후에는 내가 어떻게 활용하는 지에 대해서 서술하고자 한다.