# Story 작성

## Story란? <a href="#what-is-story" id="what-is-story"></a>

Story는 UI 컴포넌트의 렌더링 된 상태를 캡처합니다. 인자 집합이 주어지면 컴포넌트 상태를 반환하는 함수입니다.

{% hint style="info" %}
Storybook은 React 또는 Vue의 **props**, Angular의 **@Input** 및 기타 유사한 개념(전달 된 속성)을 일반적인 용어인 인자(줄여서 **args**)를 사용합니다.
{% endhint %}

![](/files/-MVW8dBiA74HDYPiZ5P8)

## Story 구성 <a href="#configure-story" id="configure-story"></a>

Storybook은 컴포넌트와 그 하위 스토리의 2가지 기본 단계로 구성되어 있습니다. 스토리는 컴포넌트에 대한 개별 이야기입니다. 필요한 만큼의 스토리를 컴포넌트 별로 작성할 수 있습니다.

```python
컴포넌트
├── 스토리
├── 스토리
├── ...
└── 스토리
```

## Story 파일 생성 <a href="#create-story-file" id="create-story-file"></a>

Story는 컴포넌트 파일이 위치한 디렉토리 안에 작성합니다. 이 파일은 개발용이며 프로덕션 번들에 포함되지 않습니다. Story를 구성할 컴포넌트를 작성한 후, 컴포넌트 파일과 같은 위치에 Story 파일을 추가합니다.

{% tabs %}
{% tab title="디렉토리 구조" %}

```python
components/
└─ StyleInput/
   ├─ StoryInput.scss
   ├─ StyleInput.js
   └─ StyleInput.stories.js # Story 파일
```

{% endtab %}
{% endtabs %}

## Story 파일 포멧 <a href="#story-file-format" id="story-file-format"></a>

[컴포넌트 스토리 포멧(CSF)](https://storybook.js.org/docs/react/api/csf)은 Story를 구성하는 객체를 말하며, 작성할 Story의 정보를 작성합니다.

| **속성**                                                                                          | **설명**                                         |
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| [title](https://storybook.js.org/docs/react/configure/sidebar-and-urls#permalinking-to-stories) | Storybook 앱 사이드바에 표시되는 컴포넌트 이름                 |
| [component](https://storybook.js.org/docs/react/writing-docs/docs-page#component-parameter)     | Story를 작성 할 컴포넌트 설정 (컴포넌트 설명, `props` 추출)      |
| [args](https://storybook.js.org/docs/react/writing-stories/args#gatsby-focus-wrapper)           | 모든 Story에 공통 적용할 전달 인자 설정                      |
| [argTypes](https://storybook.js.org/docs/react/api/argtypes)                                    | 각 Story 인자(args)의 행동(behaviour) 방식 설정          |
| [decorators](https://storybook.js.org/docs/react/writing-stories/decorators)                    | Story를 감싸는 렌더링 함수 (Story 보강, 렌더링 세부 정보 수집 등)   |
| [parameters](https://storybook.js.org/docs/react/writing-stories/parameters#story-parameters)   | Story에 대한 정적 메타 데이터 정의 (다양한 애드온 구성 제공)         |
| [excludeStories](https://storybook.js.org/docs/react/api/csf#non-story-exports)                 | Storybook에서 Story를 내보낼 때 렌더링에서 제외 설정 (정규 표현 식) |

컴포넌트 스토리 포멧(CSF)은 "[기본(default) 내보내기](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/export)"로 내보내야 합니다.

{% tabs %}
{% tab title="Component Story Format (CSF)" %}

```javascript
export default {
  title: '시스템/그룹/컴포넌트 이름',
  component: Component,
  // args: {},
  // argTypes: {}
  // decorators: []
  // ...
}
```

{% endtab %}
{% endtabs %}

## Story 작성(정의) <a href="#define-story" id="define-story"></a>

컴포넌트의 Story(CSF)는 "[지정 된 이름(named) 내보내기](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/export)"를 사용하여 컴포넌트 Story를 정의합니다. Story 이름은 TitleCase로 작성하는 것이 권장됩니다.

```jsx
export const Story = () => <Button secondary>스토리 버튼</Buton>
```

Storybook에 표시되는 Story 이름을 변경해야 한다면? `storyName` 속성을 사용할 수 있습니다.

```jsx
StoryButton.storyName = 'Secondary Button'
```

## 멀티 Story 작성 <a href="#write-multi-story" id="write-multi-story"></a>

Story는 컴포넌트를 렌더링 하는 방법을 설명하는 함수입니다. 컴포넌트 마다 여러 개의 Story를 가질 수 있습니다. Story를 만드는 가장 간단한 방법은 인자가 다른 컴포넌트를 여러 번 렌더링하는 것입니다.

```jsx
export const Primary = () => <Button children="프라이머리 버튼" />
export const Secondary = () => <Button secondary children="세컨더리 버튼" />
export const Tertiary = () => <Button tertiary children="터시어리 버튼" />
```

{% hint style="warning" %}
이 방법은 작성이 간단하지만, 많은 Story를 관리해야 할 경우 유지보수가 어렵습니다.
{% endhint %}

## 템플릿, 인자 활용 <a href="#using-template-args" id="using-template-args"></a>

하나 이상 컴포넌트의 스토리를 만들 경우 `Template` 변수에 컴포넌트 복사본을 할당하는 것이 편리합니다. 이 패턴을 Story에 도입하면 작성 또는 유지보수 해야 할 코드 양이 줄어듭니다.

```jsx
// Story 템플릿
const Template = (args) => <Button {...args} />

// 템플릿 복사본
export const Primary = Template.bind({})
export const Secondary = () => Template.bind({})
export const Tertiary = () => Template.bind({})

// 복사한 각 템플릿(Copyed Story)에 인자 설정
Primary.args = { children: "프라이머리 버튼" }
Secondary.args = { secondary: true, children: "세컨더리 버튼" }
Tertiary.args = { tertiary: true, children: "터시어리 버튼" }
```

{% hint style="info" %}
**Template.bind({ })**&#xB294; 함수의 복사본을 만드는 [표준 JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)의 기법입니다. 이 기법을 사용하여 각각의 스토리가 고유한 속성(properties)을 갖지만, 동시에 동일한 구현을 사용하도록 할 수 있습니다.
{% endhint %}

인자(arguments)는 줄여서 `args`를 사용하며, Storybook을 다시 시작하지 않고도 Controls addon으로 컴포넌트를 실시간으로 수정할 수 있습니다. 값이 변하면 컴포넌트도 실시간 업데이트 됩니다.

{% hint style="warning" %}
**Primary.args**에서 의문이 들 수 있습니다. args는 JavaScript 함수의 표준 속성이 아니기 때문이죠. 이는 Storybook이 컴포넌트를 렌더링 하는 과정에서 Primary에 **args**를 전달하므로 정상 작동하는 것입니다.
{% endhint %}

아래 코드는 Story 작성 예시입니다.

{% tabs %}
{% tab title="StoryInput.stories.js" %}

```jsx
// Story를 구성할 컴포넌트 파일 불러오기
import StoryInput from './StoryInput'

/* ------------------------------------------------------------------- */

export default {
  // 컴포넌트 설명을 입력하면 Storybook에 카테고리 되어 표시됩니다.
  title: 'FormControl/StoryInput',
  // 컴포넌트 설정
  component: StoryInput,
  // 전달인자 공통 설정
  args: {
    label: '이메일',
    type: 'email',
    placeholder: 'yamoo9@euid.dev',
  },
  // 전달 인자 유형 설정
  argTypes: {
    backgroundColor: { control: 'color' },
    disabled: { control: 'boolean' },
  },
}

// 컴포넌트 템플릿
// 함수의 복사본을 만드는 표준 JavaScript 기법
const Template = (args) => <StoryInput {...args} />

// sm 사이즈 컴포넌트
export const SmSize = Template.bind({})
SmSize.storyName = 'Small'
SmSize.args = {
  id: 'sm-size-kwdj1',
  size: 'sm',
}
SmSize.parameters = {
  viewport: {
    defaultViewport: 'iphonex',
  },
}

// md 사이즈 컴포넌트
export const MdSize = Template.bind({})
MdSize.storyName = 'Medium'
MdSize.args = {
  id: 'md-size-kwdj5',
  size: 'md',
}
MdSize.parameters = {
  viewport: {
    defaultViewport: 'iphonexr',
  },
}

// lg 사이즈 컴포넌트
export const LgSize = Template.bind({})
LgSize.storyName = 'Large'
LgSize.args = {
  id: 'lg-size-kwdj8',
  size: 'lg',
}
```

{% endtab %}

{% tab title="StoryInput.js" %}

```javascript
import React from 'react'
import PropTypes from 'prop-types'
import './StoryInput.scss'

// Story를 구성할 컴포넌트를 작성합니다.
const StoryInput = ({ id, label, className, size, ...restProps }) => (
  <div className={`storyInput ${className} ${size}`.trim()}>
    <label htmlFor={id}>{label}</label>
    <input id={id} type="text" {...restProps} />
  </div>
)

export default StoryInput

// 컴포넌트 속성 검사를 설정하면 Story 문서에 반영됩니다.
// 컴포넌트에 필요한 데이터 형태를 명시하려면 React에서 propTypes를 사용하는 것이 가장 좋습니다.
// 이는 자체적 문서화일 뿐만 아니라, 문제를 조기에 발견하는 데 도움이 됩니다.
StoryInput.propTypes = {
  /** label 요소와 input 요소를 연결하는 key */
  id: PropTypes.string.isRequired,
  /** UI에 표시되는 레이블 */
  label: PropTypes.string.isRequired,
  /** 레이블을 UI에서 감춤 (스크린 리더 사용자에게는 읽힘) */
  labelHidden: PropTypes.bool,
  /** 플레이스홀더 */
  placeholder: PropTypes.string,
  /** 커스텀 클래스 이름 */
  className: PropTypes.string,
  /** 설정 가능한 인풋 타입 */
  type: PropTypes.oneOf(['text', 'email', 'password', 'search']),
  /** 인풋 크기 */
  size: PropTypes.oneOf(['sm', 'md', 'lg']),
}

// 컴포넌트 기본 속성을 설정하면 Story 문서에 반영됩니다.
StoryInput.defaultProps = {
  type: 'text',
  className: '',
  size: 'md',
  labelHidden: false,
}
```

{% endtab %}

{% tab title="StoryInput.scss" %}

```css
@use 'sass:map';

$colors: (
  dark: (
    label: #767f96,
    input: (
      border: #1f57e7,
      bg: #292f3a,
      fg: #f5f5f5,
    ),
  ),
  light: (
    label: #595d65,
    input: (
      border: #9f9da9,
      bg: #fdfdfd,
      fg: #08163a,
    ),
  ),
);

@function getLabelColor($theme-name: light) {
  $theme: map.get($colors, $theme-name);
  @return map.get($theme, label);
}

@function getInputColor($name, $theme-name: light) {
  $theme: map.get($colors, $theme-name);
  $input: map.get($theme, input);
  @return map.get($input, $name);
}

.storyInput {
  $size: 14px;

  display: inline-flex;
  flex-direction: column;

  label {
    margin-bottom: 0.4em;
    color: getLabelColor();
  }

  input {
    border: 2px solid rgba(getInputColor(border), 0.4);
    border-radius: 8px;
    padding: 1em;
    background: getInputColor(bg);
    color: getInputColor(fg);

    &:focus {
      outline: 0;
      border: 2px solid getInputColor(border);
      box-shadow: 2px solid rgba(getInputColor(border), 0.4);
    }
  }

  // Dark Mode
  .dark & {
    label {
      color: getLabelColor(dark);
    }
    input {
      border-color: rgba(getInputColor(border, dark), 0.4);
      background: getInputColor(bg, dark);
      color: getInputColor(fg, dark);

      &:focus {
        border-color: getInputColor(border, dark);
        box-shadow: 2px solid rgba(getInputColor(border, dark), 0.4);
      }
    }
    ::placeholder {
      color: #767f96;
    }
  }

  // Size

  &.sm {
    label,
    input {
      font-size: $size * 0.8;
    }
  }

  &.md,
  input {
    label {
      font-size: $size;
    }
  }

  &.lg {
    label,
    input {
      font-size: $size * 1.2;
    }
  }
}
```

{% endtab %}
{% endtabs %}

## Storybook 구동 <a href="#run-storybook" id="run-storybook"></a>

storybook 구동 명령을 사용해 Storybook을 웹 브라우저에서 확인할 수 있습니다.

```bash
npm run storybook
```

#### Light 모드

![](/files/-MVW5jGerMj-NK4a166g)

#### Dark 모드 (별도 설정 필요)

![](/files/-MVW5pOEi7z91ViiIhMW)

## Storybook 스니펫 <a href="#snippets" id="snippets"></a>

[Storybook 컴포넌트 스니펫](https://snippet-generator.app/?description=Storybook+%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8+Story\&tabtrigger=sb-component\&snippet=import+%241+from+%27.%2F%241%27%0A%0Aconst+storyConfig+%3D+%7B%0A++%2F%2F+%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8+%EC%84%A4%EB%AA%85%0A++title%3A+%27%24%7B2%3A%EC%8B%9C%EC%8A%A4%ED%85%9C%7D%2F%24%7B3%3A%EA%B7%B8%EB%A3%B9%7D%2F%241%27%2C%0A++%2F%2F+%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8+%EC%84%A4%EC%A0%95%0A++component%3A+%241%2C%0A++%2F%2F+%EC%A0%84%EB%8B%AC+%EC%9D%B8%EC%9E%90+%EC%84%A4%EC%A0%95%0A++%2F%2F+args%3A+%7B%0A++%2F%2F+++%EC%84%A4%EC%A0%95+%EC%98%88%EC%8B%9C+%0A++%2F%2F+++type%3A+%27email%27%2C%0A++%2F%2F+%7D%2C%0A++%2F%2F+%EC%A0%84%EB%8B%AC+%EC%9D%B8%EC%9E%90+%EC%9C%A0%ED%98%95+%EC%84%A4%EC%A0%95%0A++%2F%2F+argTypes%3A+%7B%0A++++%2F%2F+%EC%BB%AC%EB%9F%AC+%ED%94%BC%EC%BB%A4+%EC%BB%A8%ED%8A%B8%EB%A1%A4+%EC%84%A4%EC%A0%95+%EC%98%88%EC%8B%9C%0A++++%2F%2F+backgroundColor%3A+%7B+control%3A+%27color%27+%7D%2C%0A++%2F%2F+%7D%0A%7D%0A%0Aexport+default+storyConfig%0A%0A%2F%2F+%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8+%ED%85%9C%ED%94%8C%EB%A6%BF%0Aconst+Template+%3D+%28args%29+%3D%3E+%3C%241+%7B...args%7D+%2F%3E%0A%0A%2F%2F+%EC%98%88%EC%A0%9C+%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8+%EC%83%9D%EC%84%B1%0Aexport+const+Example+%3D+Template.bind%28%7B%7D%29%0A%2F%2F+%EC%98%88%EC%A0%9C+%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8+props+%EC%84%A4%EC%A0%95%0AExample.args+%3D+%7B%0A++%2F%2F+prop+%EC%86%8D%EC%84%B1+%EC%84%A4%EC%A0%95%0A%7D\&mode=vscode)을 사용하면 손쉽게 컴포넌트 Story를 구성할 수 있습니다.

```jsx
import $1 from './$1'

const storyConfig = {
  // 컴포넌트 설명
  title: '${2:시스템}/${3:그룹}/$1',
  // 컴포넌트 설정
  component: $1,
  // 전달 인자 설정
  // args: {
  //   설정 예시 
  //   type: 'email',
  // },
  // 전달 인자 유형 설정
  // argTypes: {
    // 컬러 피커 컨트롤 설정 예시
    // backgroundColor: { control: 'color' },
  // }
}

export default storyConfig

// 컴포넌트 템플릿
const Template = (args) => <$1 {...args} />

// 예제 컴포넌트 생성
export const Example = Template.bind({})
// 예제 컴포넌트 props 설정
Example.args = {
  // prop 속성 설정
}
```

## 매개변수 활용 <a href="#using-parameters" id="using-parameters"></a>

매개변수는 Story에 대한 정적 메타 데이터를 정의하는 Storybook의 방법입니다. Story의 매개변수를 사용하여 Story 또는 Story 그룹 레벨에서 다양한 애드온에 구성을 제공 할 수 있습니다. 예를 들어 앱의 다른 컴포넌트와 다른 배경에서 컴포넌트를 테스트 하고 싶다면? 다음과 같이 CSF를 설정합니다. (컴포넌트 레의 매개변수 설정)

```jsx
export default {
  title: 'Button',
  component: Button,
  //👇 Story 매개 변수 설정
  parameters: {
    backgrounds: {
      values: [
        { name: 'darkred', value: '#340000' },
        { name: 'storypink', value: '#fb6597' }
      ],
    },
  },
};
```

![](/files/-MVXl5wKYHYX-u3BXmhu)

## 데코레이터 활용 <a href="#using-decorators" id="using-decorators"></a>

데코레이터는 Story를 렌더링 할 때 임의의 마크업으로 컴포넌트를 감싸는 메커니즘입니다. 예를 들어 테마 또는 레이아웃 래퍼(wrapper)가 필요할 수 있습니다. 또는 UI에 특정 컨텍스트 혹은 데이터 공급자(provider)가 필요할 수 있습니다.

간단한 예는 컴포넌트의 Story에 스타일 래퍼를 추가하는 것입니다. 다음과 같이 Story를 감싸는 데코레이터를 작성하면 Storybook 뷰포트에 스타일이 반영된 래퍼 요소가 렌더링 됩니다.

```jsx
export default {
  title: 'Button',
  component: Button,
  decorators: [
    (Story) => (
      <div style={{ margin: '40px' }}>
        <Story />
      </div>
    ),
  ],
};
```

{% hint style="info" %}
보다 복잡한 데코레이터 구성 예(ThemeProvider 등)를 [참고](https://storybook.js.org/docs/react/writing-stories/decorators#context-for-mocking)하세요.
{% endhint %}

## 멀티 컴포넌트 Story <a href="#multi-components-story" id="multi-components-story"></a>

디자인 시스템 또는 컴포넌트 라이브러리를 빌드 할 때 함께 작동하도록 디자인 된 2개 이상 컴포너트의 Story를 작성하려면 상위 컴포넌트와 하위 컴포넌트 모두 불러와 사용하고, 컴포넌트 작동 상황 별 Story를 렌더링 하는 것이 좋습니다.

{% tabs %}
{% tab title="List.stories.js" %}

```jsx
import List from './List';
import ListItem from './ListItem';

export default {
  component: List,
  title: 'List',
};

// 하위 컴포넌트를 포함하지 않은 상위 컴포넌트 Story
export const Empty = (args) => <List {...args} />;

// 1개의 하위 컴포넌트를 포함한 상위 컴포넌트 Story
export const OneItem = (args) => (
  <List {...args}>
    <ListItem />
  </List>
);

// 1개 이상 하위 컴포넌트를 포함한 상위 컴포넌트 Story
export const ManyItems = (args) => (
  <List {...args}>
    <ListItem />
    <ListItem />
    <ListItem />
  </List>
);
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
컴포넌트 구성이 보다 복잡하다면 [멀티 컴포넌트 Story 워크플로우](https://storybook.js.org/docs/react/workflows/stories-for-multiple-components)를 참고하세요.
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yamoo9.gitbook.io/learning-react-app/tip-and-references/storybook-for-react/write-story.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
