ConnecTo

2022/11/18 - Story book

codevil 2022. 11. 18. 19:48

What is Story

Story는 컴포넌트가 UI에 렌더링 된 상태를 캡처하고, 인자(args)가 주어지면 컴포넌트 상태를 반환하는 함수입니다.

Story Process

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

Editable Config

  • title
    • Storybook 사이드 바에 표시되는 컴포넌트 이름
  • component
    • Story를 작성할 컴포넌트 작성
  • args
    • 모든 Story에 공통 적용할 전달 인자 설정
  • argTypes
    • 각 Story 인자의 행동 방식 설정
  • decorators
    • Story를 감싸는 렌더링 함수
  • parameters
    • Story에 대한 정적 메타 데이터 정의
  • excludeStories
    • Story를 내부낼 때 렌더링에서 제외 설정

How to use

Story 작성

export const SecondaryButton = () => <Button />
//Story이름은 TitleCase로 작성되는 것이 권장됩니다.
//만일 Storybook에 표시되는 Story의 이름을 변경해야한다면

SecondaryButton.storyName = 'ChangedName'
//이름을 바꾸려면 storyName을 이용해서 바꿔야 한다.

export const Tetiary = () => <Button/>

Story Template

const Template = (args) => <Button {...args/>

인자(arguments)는 줄여서 args를 사용하며, Storybook을 다시 시작하지 않아도, 컨트롤(control) 애드온을 사용해 컴포넌트를 실시간 업데이트 하여 화면에 표시할 수 있습니다.

Copied Template

export const Primary = Template.bind({});
export const Secondary = Template.bind({});
export const Tertiary = Template.bind({});

Template.bind({})는 함수의 복사본을 만드는 JavaScript의 표준 기법입니다. 이 방법을 사용하면 각 story가 고유한 속성(properties)을 갖지만, 동시에 동일한 구현을 사용하도록 할 수 있습니다.

 

Story Example

Story.stories.js

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

export default Meta;

// 컴포넌트 템플릿
// 함수의 복사본을 만드는 표준 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',
};

Story.scss

@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;
    }
  }
}

Story.js

import './StoryInput.scss';
import { string, bool, oneOf } from 'prop-types';

// 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: string.isRequired,
  /** UI에 표시되는 레이블 */
  label: string.isRequired,
  /** 레이블을 UI에서 감춤 (스크린 리더 사용자에게는 읽힘) */
  labelHidden: bool,
  /** 플레이스홀더 */
  placeholder: string,
  /** 커스텀 클래스 이름 */
  className: string,
  /** 설정 가능한 인풋 타입 */
  type: oneOf(['text', 'email', 'password', 'search']),
  /** 인풋 크기 */
  size: oneOf(['sm', 'md', 'lg']),
};

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

 

Setting to start

{
  "scripts": {
    "storybook": "start-storybook -p 6006 -s public",
    "build-storybook": "build-storybook -s public"
  }
}

Usage of parameters

const Meta = {
  title: 'Button',
  component: Button,

  parameters: {
    backgrounds: {
      values: [
        { name: 'darkred', value: '#340000' },
        { name: 'storypink', value: '#fb6597' }
      ],
    },
  },

};

export default Meta;

Usage of decorators

const Meta = {
  title: 'Button',
  component: Button,

  decorators: [
    (Story) => (
      <div style={{ margin: '40px' }}>
        <Story />
      </div>
    ),
  ],

};

export default Meta;