๐จ 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์ ์ฌ์ฉํ๊ธฐ ์ํด์๋, ์ปดํผ๋ํธ๋ค์ด ๋ ๋ฆฝ์ ์ด์ด์ผ ํ๊ณ ๋น์ง๋์ค๋ก์ง๊ณผ ๋ณ๊ฐ๋ก ๋์ํด์ผํ๋ค.
๊ทธ๋์ผ ํ ์คํธํ๊ธฐ๊ฐ ์ฉ์ดํ๊ณ ์ ์ง๋ณด์์ ํธํ๋ค
๋๊ธ