๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
StoryBook

๐Ÿš› vue2 + storybook6

by frontChoi 2025. 6. 29.
๋ฐ˜์‘ํ˜•

๐Ÿšจ StoryBook์ด๋ž€

์ปดํผ๋„ŒํŠธ๋ฅผ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋Š” ํˆด, AppButton์ด๋ผ๋Š” ๊ฒƒ์ด ์žˆ์„๋•Œ props์— ๋”ฐ๋ผ ๋ณด์—ฌ์ง€๋Š”๊ฒƒ์ด ๋‹ค๋ฅธ๋ฐ ๊ทธ๊ฒƒ๋“ค์„ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์šฉ์ดํ•˜๋‹ค.

๐Ÿš” ๋„์ž…๋ฐฐ๊ฒฝ

์‚ฌ๋‚ด์— ๊ธฐ์กด์—๋Š” ๋ฒ„ํŠผ๋“ค์ด css๋งŒ ๊ณต์œ ํ•˜๊ณ  ๊ทธ css๋„ ์ž˜๋ชป ๋ณต๋ถ™ํ•˜๊ฑฐ๋‚˜ ํ–ˆ์„๋•Œ, ๋ฒ„ํŠผ์— ํ˜•ํƒœ๊ฐ€ ์กฐ๊ธˆ์”ฉ ๋‹ฌ๋ผ์กŒ๋‹ค. ๊ทธ๋ฆฌํ•˜์—ฌ ์ปดํผ๋„ŒํŠธํ™” ์‹œํ‚ค๊ณ  ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ํ•„์š”์„ฑ์„ ๋А๊ผˆ๋‹ค ๊ทธ๋ฆฌ๊ณ  props๋ฅผ ์–ด๋–ค๊ฒƒ์„ ๋ฐ›๋Š”์ง€ ๋ฌธ์„œํ™” ์‹œํ‚ฌ ํ•„์š”๊ฐ€ ์žˆ์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋””์ž์ด๋„ˆ <=> ๊ฐœ๋ฐœ์ž๊ฐ„์˜ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜์ด ํ•„์š”ํ•œ๋ฐ, ๊ธฐ์ค€์ด๋˜๋Š” ๋ฌธ์„œ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ , ํ•˜๋‚˜์˜ ๋ฌธ์„œ๊ฐ€์ง€๊ณ  ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ ํ•˜๊ธฐ์— ์šฉ์ดํ–ˆ๋‹ค.

๐Ÿšก ํ™˜๊ฒฝ์„ธํŒ…

vue2 ๋ฒ„์ „์— ๋งž๋Š” ํŒจํ‚ค์ง€ ์„ค์น˜

๊ธฐ์กด์— ํ”„๋กœ์ ํŠธ๊ฐ€ vue2์ด๋ฏ€๋กœ vue2 ๊ธฐ์ค€์œผ๋กœ ๊ด€๋ จ ํŒจํ‚ค์ง€ ์„ค์น˜๊ฐ€ ํ•„์š”ํ•˜์˜€๋‹ค. ์‚ฌ๋‚ด ํ”„๋กœ์ ํŠธ์˜ ์ฝ”๋“œ๋“ค์„ ์˜ฌ๋ฆด ์ˆ˜ ์—†์œผ๋ฏ€๋กœ vue2ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑํ•˜์˜€๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ด€๋ จ ์žˆ๋Š” ํŒจํ‚ค์ง€๋“ค์„ ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค์น˜ํ•˜์˜€๋‹ค

npm install --save-dev @storybook/vue@6.5.16 @storybook/addon-actions@6.5.16 @storybook/addon-essentials@6.5.16 @storybook/addon-links@6.5.16 storybook-addon-designs@6.3.1

 

  • @storybook/vue@6.5.16 : storybook vue ์ „์šฉ ์„ค์น˜
  • @storybook/addon-actions@6.5.16  :  storybook action(click ๋“ฑ ์ด๋ฒคํŠธ ์‚ฌ์šฉ)
  •  @storybook/addon-essentials@6.5.16 : storybook ํ•„์ˆ˜ ํ”Œ๋Ÿฌ๊ทธ์ธ
  • storybook-addon-designs@6.3.1 : ํ”ผ๊ทธ๋งˆ์™€ ์—ฐ๊ฒฐ์„ ์œ„ํ•ด ์„ค์น˜ ํ•„์š”

 

main.js 

  • main.js์€ storybook์„ ์‹คํ–‰์‹œํ‚ค๊ธฐ ์œ„ํ•œ ํŒŒ์ผ์ด๋‹ค. ์œ„์น˜๋Š” root/.storybook/main.js์ด๋‹ค
const path = require("path");

module.exports = {
  // ์–ด๋–ค ํŒŒ์ผ๋“ค์„ Storybook์ด **์Šคํ† ๋ฆฌ(์ปดํฌ๋„ŒํŠธ ์˜ˆ์ œ)**๋กœ ์ธ์‹ํ• ์ง€ ํŒจํ„ด ์ง€์ •
  stories: ["../src/**/*.stories.@(js|ts|vue|mdx)"],
  // ์‚ฌ์šฉํ•  addon ๋ชฉ๋ก ๋ฐฐ์—ด ์ •์˜
  addons: [
    "@storybook/addon-essentials", // ํ•„์ˆ˜ ์• ๋“œ์˜จ ๋ฌถ์Œ
    "@storybook/addon-docs", // ๋ฌธ์„œ ์ž๋™ ์ƒ์„ฑ
    "storybook-addon-designs", // ๋””์ž์ธ ์‹œ์•ˆ(Figma ๋“ฑ) ์—ฐ๊ฒฐ
  ],
  // ์–ด๋–ค ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ์ •์˜
  framework: {
    name: "@storybook/vue",
    options: {},
  },
  webpackFinal: async (config) => {
    config.resolve.alias = {
      ...(config.resolve.alias || {}),
      "@": path.resolve(__dirname, "../src"),
    };

    return config;
  },
  // ๋ฌธ์„œ ์ž๋™ ์ƒ์„ฑ ์˜ต์…˜
  docs: {
    autodocs: true,
  },
};

 

preview.js

์Šคํ† ๋ฆฌ๋“ค์ด ๋ Œ๋”๋ง๋˜๋Š” ๋ฐฉ์‹๊ณผ ํ™˜๊ฒฝ์„ ์„ค์ •ํ•˜๋Š” ํŒŒ์ผ

import "../src/assets/styles/reset.css"; // reset.css ์ ์šฉ
export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" }, // onClick, onChange ๊ฐ™์€ props๋ฅผ ์ž๋™์œผ๋กœ Action ํƒญ์— ์—ฐ๊ฒฐ
  controls: {
    matchers: {
      color: /(background|color)$/i, // ์ด๋ฆ„์ด background, color๋กœ ๋๋‚˜๋Š” props๋ฅผ "์ƒ‰์ƒ ์„ ํƒ๊ธฐ"๋กœ ์ฒ˜๋ฆฌ
      date: /Date$/, // ์ด๋ฆ„์ด Date๋กœ ๋๋‚˜๋Š” props๋ฅผ "๋‚ ์งœ ์„ ํƒ๊ธฐ"๋กœ ์ฒ˜๋ฆฌ
    },
  },
};

 

์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ ๋“ฑ๋ก

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

 

 

๐Ÿšœ AppButton ์„ storybook๋งŒ๋“ค๊ธฐ

AppButton์€ ์•ฑ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ฒ„ํŠผ์ด๋ฉฐ, ์‚ฌ์šฉ์˜ˆ์‹œ์ด๋‹ค. ์•„๋ž˜๋Š” AppButton ํŒŒ์ผ์ด๋‹ค

  • props ์œผ๋กœ color(primary,success,warning), size(lg,md,sm),disabled(true,false)๋ฅผ ๋ฐ›๋Š”๋‹ค
  • click,focus์ด๋ฒคํŠธ๊ฐ€ ์กด์žฌํ•œ๋‹ค.
<template>
  <div>
    <button
      class="app-button"
      :disabled="disabled"
      :class="[...css]"
      @focus="handleFocus"
      @click="onClickBtn"
    >
      ๋ฒ„ํŠผ
    </button>
  </div>
</template>
<script>
export default {
  props: {
    color: {
      type: String,
      default: "primary", // primary , success , warning
      required: false,
    },
    size: {
      type: String,
      default: "md", // lg , md , sm
      required: false,
    },
    disabled: {
      type: Boolean,
      default: false,
      required: false,
    },
  },
  emits: ["click", "focus"],
  methods: {
    onClickBtn() {
      this.$emit("click");
    },
    handleFocus() {
      this.$emit("focus");
    },
  },
  computed: {
    css() {
      return [this.color, this.size];
    },
  },
};
</script>
<style scoped>
.app-button {
  font-size: 16px;
  cursor: pointer;
  line-height: 1.2;
  border: none;
  box-sizing: border-box;
  font-weight: 400;
  text-align: center;
  user-select: none;
  border: 1px solid transparent;
  display: inline-block;
  width: 100%;
}

.app-button:disabled {
  opacity: 0.65;
  cursor: initial;
}

/** ๋ฒ„ํŠผ ์ƒ‰์ƒ */
.app-button.primary {
  background-color: #007bff;
  color: white;
}
.app-button.success {
  background-color: #28a745;
  color: white;
}
.app-button.warning {
  background-color: #ffc107;
  color: white;
}

/* ๋ฒ„ํŠผ ์‚ฌ์ด์ฆˆ */
.app-button.lg {
  padding: 0.75rem 1.25rem;
  font-size: 1.5rem;
  line-height: 1.5;
  border-radius: 0.4rem;
}

.app-button.md {
  padding: 0.5rem 1rem;
  font-size: 1.25rem;
  line-height: 1.5;
  border-radius: 0.3rem;
}

.app-button.sm {
  padding: 0.25rem 0.5rem;
  font-size: 0.875rem;
  line-height: 1.5;
  border-radius: 0.2rem;
}
</style>

 

๊ทธ๋ฆฌ๊ณ  ์•„๋ž˜ ์ฝ”๋“œ๋Š” AppButton.stories.js์ด๋‹ค

import AppButton from "./AppButton.vue";

export default {
  title: "Button/AppButton",
  component: AppButton,
  // argTypes
  argTypes: {
    color: {
      control: {
        type: "select",
        options: ["primary", "success", "warning"], // ์„ ํƒ ๊ฐ€๋Šฅํ•œ ํ•ญ๋ชฉ๋“ค
      },
    },
    size: {
      control: {
        type: "select",
        options: ["lg", "md", "sm"], // ์„ ํƒ ๊ฐ€๋Šฅํ•œ ํ•ญ๋ชฉ๋“ค
      },
    },
    disabled: {
      control: "boolean",
      description: "disabled ์—ฌ๋ถ€",
      defaultValue: false,
    },
    click: { action: "clicked" }, // โœ… click ์ด๋ฒคํŠธ๋ฅผ actions๋กœ ์—ฐ๊ฒฐ
    focus: { action: "focus" }, // โœ… focus ์ด๋ฒคํŠธ๋ฅผ actions๋กœ ์—ฐ๊ฒฐ
  },
};
const Template = (args, { argTypes }) => ({
  components: { AppButton },
  props: Object.keys(argTypes),
  template: '<AppButton v-bind="$props"  @click="click" @focus="focus"/>',
});

export const Default = Template.bind({});
Default.args = {
  color: "primary",
  size: "md",
};
  • argTypes์œผ๋กœ color, size,disabled ,click,focus๋ฅผ ๋„ฃ๋Š”๋‹ค
    • color,size์€ ์„ ํƒ๊ฐ’์ด ์ •ํ•ด์ ธ์žˆ์œผ๋ฏ€๋กœ control์„ type:select์™€, options์— ์‚ฌ์šฉํ•  ๊ฐ’์„ ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ๋กœ ๋„ฃ๋Š”๋‹ค
  • Template๋ผ๋Š” ๋ณ€์ˆ˜์— components,props,template์— ๊ฐ’์„ ๋„ฃ๋Š”๋‹ค
  • export const Default = Template.bind({})๋ฅผ ํ•ด์•ผ storybook์‹คํ–‰์‹œํ‚ฌ๋•Œ ํ™”๋ฉด์— ๋ณด์—ฌ์ง„๋‹ค

 

 

AppButton ๊ฒฐ๊ณผ

Button์˜ primary,success,warning , lg,md,sm, disabled์˜ ๊ฒฐ๊ณผ๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์ด ํ™”๋ฉด์—์„œ ๋ณด์—ฌ์ง„๋‹ค

  • Primary

  • Success

 

  • Warning

  • lg

  • md

  • sm

  • disabled

๐Ÿฉผ  ๋…๋ฆฝ์ ์ธ ์ปดํผ๋„ŒํŠธ๋“ค์„ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค

์ปดํผ๋„ŒํŠธ๋“ค์ด storybook์— ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”, ์ปดํผ๋„ŒํŠธ๋“ค์ด ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ•˜๊ณ  ๋น„์ง€๋‹ˆ์Šค๋กœ์ง๊ณผ ๋ณ„๊ฐœ๋กœ ๋™์ž‘ํ•ด์•ผํ•œ๋‹ค.

๊ทธ๋ž˜์•ผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ๊ฐ€ ์šฉ์ดํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜์— ํŽธํ•˜๋‹ค

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€