npx -p @storybook/cli sb init --type vue
.storybook
, ./scr/storybook
폴더가 생성되며 package.json에 storybook, build-storybook 명령어가 추가로 자동 생성됨npm run storybook
으로 6006포트에서 storybook 페이지가 열리는지 확인설치 날짜에 따라 버전은 다를 수 있으므로 참고만
"scripts": {
...
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"dependencies": {
...(생략)
},
"devDependencies": {
...
"@storybook/addon-actions": "^6.5.8",
"@storybook/addon-essentials": "^6.5.8",
"@storybook/addon-interactions": "^6.5.8",
"@storybook/addon-links": "^6.5.8",
"@storybook/builder-vite": "^0.1.36",
"@storybook/testing-library": "^0.0.11",
"@storybook/vue3": "^6.5.8"
}
}
src/storybook
하위 폴더에 기본으로 생성되는 파일 중 example 파일 (introduce 파일)은 우선 삭제하지 않고 그 외 모든 파일은 js 파일이기 때문에 삭제해도 됨 (우리는 ts 파일로 생성 할 것!)src/components/ButtonComp.vue
파일 생성<template>
<q-btn
:label="props.label"
flat
:disable="props.disable"
@click="emit('click:clicked')"
/>
</template>
<script lang="ts" setup>
import { defineProps, ref } from 'vue';
// emit
const emit = defineEmits(['click:clicked']);
// button 인터페이스
interface Button {
label?: string; // 버튼 라벨
disable?: boolean; // disable 여부
}
const props = withDefaults(defineProps<Button>(), {
label: '버튼 라벨명',
disable: false,
});
</script>
src/storybook/button.stories.ts
파일 생성import GButton from '@/components/ButtonComp.vue';
import { Meta, StoryFn } from '@storybook/vue3';
export default {
title: 'Atoms/Button',
component: GButton,
argTypes: {
label: { control: 'text' },
disable: { control: 'boolean' },
},
} as Meta<typeof GButton>;
// 기본
export const Basic: StoryFn<typeof GButton> = (args) => ({
components: { GButton },
setup() {
return { args };
},
template: `
<g-button v-bind="args"/>
`,
});
// 라벨
export const Label: StoryFn<typeof GButton> = (args) => ({
components: { GButton },
setup() {
return { args };
},
template: `
<g-button v-bind="args"/>
`,
});
Label.args = {
label: '라벨명',
};
// 선택 불가
export const Disable: StoryFn<typeof GButton> = (args) => ({
components: { GButton },
setup() {
return { args };
},
template: `
<g-button v-bind="args"/>
`,
});
Disable.args = {
disable: true,
};
.storybook/withSource.js
파일 작성코드 참고
주의사항 = lodash 설치 필요
import { addons, makeDecorator } from '@storybook/addons';
import kebabCase from 'lodash';
import { h, onMounted } from 'vue';
// this value doesn't seem to be exported by addons-docs
export const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`;
function templateSourceCode(
templateSource,
args,
argTypes,
replacing = 'v-bind="args"'
) {
const componentArgs = {};
for (const [k, t] of Object.entries(argTypes)) {
const val = args[k];
if (
typeof val !== 'undefined' &&
t.table &&
t.table.category === 'props' &&
val !== t.defaultValue
) {
componentArgs[k] = val;
}
}
const propToSource = (key, val) => {
const type = typeof val;
switch (type) {
case 'boolean':
return val ? key : '';
case 'string':
return `${key}="${val}"`;
default:
return `:${key}="${val}"`;
}
};
return templateSource.replace(
replacing,
Object.keys(componentArgs)
.map((key) => ' ' + propToSource(kebabCase(key), args[key]))
.join('')
);
}
export const withSource = makeDecorator({
name: 'withSource',
wrapper: (storyFn, context) => {
const story = storyFn(context);
// this returns a new component that computes the source code when mounted
// and emits an events that is handled by addons-docs
// this approach is based on the vue (2) implementation
// see https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/sourceDecorator.ts
return {
components: {
Story: story,
},
setup() {
onMounted(() => {
try {
// get the story source
const src = context.originalStoryFn().template;
// generate the source code based on the current args
const code = templateSourceCode(
src,
context.args,
context.argTypes
);
const channel = addons.getChannel();
const emitFormattedTemplate = async () => {
const prettier = await import('prettier/standalone');
const prettierHtml = await import('prettier/parser-html');
// emits an event when the transformation is completed
channel.emit(
SNIPPET_RENDERED,
(context || {}).id,
prettier.format(`<template>${code}</template>`, {
parser: 'vue',
plugins: [prettierHtml],
htmlWhitespaceSensitivity: 'ignore',
})
);
};
setTimeout(emitFormattedTemplate, 0);
} catch (e) {
console.warn('Failed to render code', e);
}
});
return () => h(story);
},
};
},
});
.storybook/main.ts
수정const { loadConfigFromFile, mergeConfig } = require('vite');
const path = require('path');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: '@storybook/vue3',
core: {
builder: '@storybook/builder-vite',
},
typescript: {
check: false,
checkOptions: {},
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
},
},
features: {
storyStoreV7: true,
},
async viteFinal(previousConfig) {
const { config } = await loadConfigFromFile(
path.resolve(__dirname, '../vite.config.ts')
);
return mergeConfig(previousConfig, {
...config,
plugins: [],
});
},
};
.storybook/preview.js
파일 수정import '@quasar/extras/roboto-font/roboto-font.css';
// These are optional
import '@quasar/extras/material-icons/material-icons.css';
// Loads the quasar styles and registers quasar functionality with storybook
import 'quasar/dist/quasar.css';
import { app } from '@storybook/vue3';
import { Quasar } from 'quasar';
import '@styles/index.scss';
import { withSource } from './withSource';
// This is also where you would setup things such as pinia for storybook
app.use(Quasar, {});
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
export const decorators = [withSource];
좋은 자료 감사합니다. Vue 3에 대한 만족도가 점점 떨어지네요...