Confeti ์๋น์ค์ ๋์์ธ ์์คํ ์ ๊ตฌ์ถํ๋๋ฐ ์คํ ๋ฆฌ๋ถ์ ์ฌ์ฉํ ์์ ์ด๋ผ, ์คํ ๋ฆฌ๋ถ์ด ๋ฌด์์ธ์ง์ ๋์ ๊ณผ์ ์ ์์ฑํฉ๋๋ค.
์คํ ๋ฆฌ๋ถ์ UI ์์์ ํ์ด์ง๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ตฌ์ถํ๊ธฐ ์ํ ํ๋ก ํธ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ค. UI ๊ฐ๋ฐ, ํ ์คํธ, ๋ฌธ์ํ์ ์ฌ์ฉํ๋ค.
Microsoft Fluent UI React Storybook ์์
๋ค์ด๊ฐ๊ธฐ ์์..
๐ก Story๋?
์คํ ๋ฆฌ๋ ์ปดํฌ๋ํธ์ ๋ค์ํ ์ํ์ ๋ณํ์ ์๊ฐ์ ์ผ๋ก ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ์์ฑ๋ ์์ ๋ผ๊ณ ํ ์ ์๋ค. ๊ฐ ์ปดํฌ๋ํธ๋ ์ฌ๋ฌ๊ฐ์ ์คํ ๋ฆฌ๋ฅผ ๊ฐ์ง ์ ์์ผ๋ฉฐ, ๊ฐ ์คํ ๋ฆฌ๋ ํด๋น ์ปดํฌ๋ํธ์ ํน์ ์ํ์ ์ธ๊ด๊ณผ ๋์์ ๊ฒ์ฆํ ์ ์๊ฒ ํด์ค๋ค.
๐ก Addon์ด๋?
Storybook์ ๊ธฐ๋ฅ์ ํ์ฅํด์ฃผ๋ ํ๋ฌ๊ทธ์ธ์ ๋งํ๋ค. ์คํ ๋ฆฌ๋ถ์์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํด์ฃผ๋ addon์ด ์์ง๋ง, ํ์์ ์ถ๊ฐ๋ก ์ค์นํด์ฃผ๋ฉด ๋๋ค.
๋ฌธ์ํ, ์ ๊ทผ์ฑ ํ ์คํธ, ์ธํฐ๋ํฐ๋ธ ์ปจํธ๋กค ๋ฑ ๋๋ถ๋ถ์ Storybook ๊ธฐ๋ฅ์ ์ ๋์จ์ผ๋ก ๊ตฌํ๋๋ค๊ณ ํ๋ค.
์ค์น ์ ์๋์ผ๋ก ์ค์น๋๋ ํ์ ์ ๋์จ
pnpm dlx storybook@next init
pnpm run storybook
main.tsx
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
};
export default config;
preview.tsx
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
์ด์ธ์๋ ๋ค์๊ณผ ๊ฐ์ ์ฌ๋ฌ ์ค์ ํ์ผ๋ค์ ์ถ๊ฐํ ์ ์๋ค.
manager.ts
: Storybook์ UI(์คํ ๋ฆฌ ๋ชฉ๋ก, ์ ๋์จ ํจ๋ ๋ฑ)๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์๋ ์ค์ ํ์ผpreview-head.html
: ์ธ๋ถ ํฐํธ๋ ์คํ์ผ์ํธ๋ฅผ ํฌํจํ ์ ์๋๋ก, <head>
ํ๊ทธ์ ์ถ๊ฐํ HTML์ ์ ์ํ๋ ํ์ผvanila-extract๋ฅผ ์ฌ์ฉํ๋ค๋ฉด, ๊ด๋ จ ํ๋ฌ๊ทธ์ธ์ ์ค์นํด์ค์ vite๊ฐ vanila-extract๋ฅผ ํด์ํ ์ ์๋๋ก ํด์ค์ผ ํ๋ค.
โ๏ธ๋ชจ๋ ธ๋ ํฌ์์ ์ข ์์ฑ์ ์ค์นํ ๋, ๋ฃจํธ ๋๋ ํ ๋ฆฌ์์ ์ค์นํ์ง ์๊ณ ๊ฐ ํจํค์ง ๋๋ ํ ๋ฆฌ์์ ๊ฐ๋ณ์ ์ผ๋ก ์ค์นํด์ผ ํ๋ค๋ ์ ์ ์!
pnpm add -D @vanilla-extract/vite-plugin
ํ๋ฌ๊ทธ์ธ ์ค์น ํ ๋ค์ ์ฝ๋๋ฅผ main.ts์ config ๊ฐ์ฒด ์์ ์ถ๊ฐํด์คฌ๋ค.
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
async viteFinal(config) {
config.plugins = config.plugins || [];
config.plugins.push(
vanillaExtractPlugin({
identifiers: ({ hash }) => `_${hash}`,
}),
);
return config;
},
decorator๋ฅผ ํ์ฉํ์ฌ global style์ ์ ์ฉํ ์ ์๋ค.
์คํ ๋ฆฌ๋ถ์์๋ ํ๋ฆฌ๋ทฐ ์ค์ ํ์ผ.storybook/preview.js
์ ์ฌ์ฉํด์ ๋ชจ๋ ์คํ ๋ฆฌ๋ฅผ ๊ทธ ์ปดํฌ๋ํธ ์์ ๋ฃ๊ณ ๊ฐ์ธ์ฃผ๋ฉด๋๋ค.
const preview: Preview = {
decorators: [
(Story) => (
<div className={themeClass}>
<Story />
</div>
),
],
},
};
๋ค์๊ณผ ๊ฐ์ด preview ๊ฐ์ฒด ๋ด์์ story๋ฅผ decorator๋ก ๊ฐ์ธ์ฃผ๋ฉด, ๋ชจ๋ story์ ๊ธ๋ก๋ฒ ์คํ์ผ์ด ์ ์ฉ๋๋ค.
์ด๋ jsx ๊ตฌ๋ฌธ ๋ถ๋ถ์ด ๋ฆฐํธ์๋ฌ๊ฐ ์ซ ๋ฌ๋๋ฐ, preview.tsx๋ก ํ์ฅ์ ๋ณ๊ฒฝ ํ import React from 'react';
ํด์ฃผ๋ฉด ํด๊ฒฐ๋๋ค.
// Button.tsx
import { ButtonHTMLAttributes } from 'react';
import { buttonStyle } from './styles.css';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'tertiary' | 'outline';
size?: 'xLarge' | 'large' | 'medium';
isDisabled?: boolean;
}
const Button = ({
variant = 'primary',
children,
size = 'medium',
isDisabled = false,
...props
}: ButtonProps) => {
return (
<button
type="button"
className={buttonStyle({ color: variant, size: size })}
disabled={isDisabled}
{...props}
>
{children}
</button>
);
};
export default Button;
์์ Button ์ปดํฌ๋ํธ์์๋ variant, size, isDiabled ์ 3๊ฐ์ง prop๊ณผ children ์์์ ๋ฐ๋ผ์ ์ธํ์ด ๋ฐ๋๋ค.
โก๏ธ ๋ฐ๋ผ์ ์คํ ๋ฆฌ๋ถ์์๋ ์ด 4๊ฐ์ ์์๊ฐ ๋ฐ๋์์ ๋ ๊ฐ๊ฐ ์ด๋ค ๋ชจ์ต์ธ์ง ํ์ธํ๋ ์คํ ๋ฆฌ๋ฅผ ๋ง๋ค๋ฉด ๋๋ค!
๋์์ธ ์์คํ ๋ด์ ํด๋ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ๋ค.
|-- src
|-- components
|-- Button
|-- Button.tsx
|-- Button.stories.tsx
|-- Button.css.ts
์คํ ๋ฆฌ๋ฅผ ์์ฑ์ ๋์์ฃผ๋ ์ปดํฌ๋ํธ. ์คํ ๋ฆฌ๋ฅผ ๊ทธ๋ฆฌ๋ ์ญํ ์ ํ์ง๋ ์์์ meta๋ก ์ต์ ๋ค์ ์ค์ ํ๊ณ story ์์ฑ์ ๋ฐ๋ก ํด์ฃผ์ด์ผ ํ๋ค.
text
: ํ
์คํธ ์
๋ ฅ ๊ฐ๋ฅnumber
: ์ซ์ ์
๋ ฅ ๊ฐ๋ฅcolor
: ์์ ์ ํ ๊ฐ๋ฅradio
: options ๊ฐ๋ค ์ ํ ๊ฐ๋ฅํ ๋ผ๋์ค ๋ฒํผselect
: options ๊ฐ๋ค ์ ํ ๊ฐ๋ฅํ ๋๋กญ๋ค์ด ๋ฉ๋ดmulti-select
: ์ฌ๋ฌ ์ต์
์ ํํ ์ ์๋ ์
๋ ํธ ๋ฐ์คboolean
: true / false ์ ํ ๊ฐ๋ฅdate
: ๋ ์ง ์ ํ๊ธฐrange
: ์ฌ๋ผ์ด๋๋ฅผ ์ฌ์ฉํ ์ซ์ ์
๋ ฅobject
: ๊ฐ์ฒด ๋๋ JSON ์
๋ ฅfile
: ํ์ผ ์
๋ก๋import type { Meta, StoryObj } from '@storybook/react';
import { ButtonHTMLAttributes } from 'react';
import Button from '../components/Button/Button';
const meta = {
// ์คํ ๋ฆฌ๋ถ์ Common/Button ํด๋ ์์ฑ
title: 'Common/Button',
// Button ์ปดํฌ๋ํธ๋ก ์คํ ๋ฆฌ ์์ฑ
component: Button,
parameters: {
layout: 'centered',
},
// ๋ฌธ์ ์๋ ์์ฑ
tags: ['autodocs'],
// Button ์ปดํฌ๋ํธ์์ ์ฐ์ด๋ props ๊ฐ๋ค์ ํ์
๋ช
์
argTypes: {
// radio ๋ฒํผ
variant: {
control: { type: 'radio' },
options: ['primary', 'secondary', 'tertiary', 'outline'],
},
size: {
control: { type: 'radio' },
options: ['xLarge', 'large', 'medium'],
},
// text ์
๋ ฅ
children: {
control: { type: 'text' },
},
// true / false ์ ํ
isDisabled: {
control: { type: 'boolean' },
},
},
// ๊ณตํต์ผ๋ก ์ฐ์ด๋ props๊ฐ ์ง์ || ๊ธฐ๋ณธ๊ฐ ์ง์
args: {
variant: 'primary',
size: 'medium',
children: 'Button',
isDisabled: false,
},
} satisfies Meta<typeof Button>;
// ์ด meta ๋ฐ์ดํฐ๋ก ์คํ ๋ฆฌ๋ฅผ ์์ฑํ ๊ฑฐ๋ค ์ ์ธ!
export default meta;
์ด๋ ๊ฒ variant๋ค์ ์๊ฐ์ ์ผ๋ก ๋ฐ๋ก ํ์ธํ ์ ์์!!
type Story = StoryObj<typeof meta>;
// meta์์ ์ค์ ํ ๊ฐ๋ค๋ก Default ์คํ ๋ฆฌ ์์ฑ
export const Default: Story = {};
// variant ๊ฐ์ ๋ฐ๋ฅธ story ์์ฑ ํจ์
const createButtonStory = (variant: ButtonProps['variant']) => ({
args: {
variant,
},
argsType: {
variant: {
control: false,
},
},
});
// Primary ํ์
๋ฒํผ ์คํ ๋ฆฌ ์์ฑ
export const Primary: Story = createButtonStory('primary');
// Secondary ํ์
๋ฒํผ ์คํ ๋ฆฌ ์์ฑ
export const Secondary: Story = createButtonStory('secondary');
// Tertiary ํ์
๋ฒํผ ์คํ ๋ฆฌ ์์ฑ
export const Tertiary: Story = createButtonStory('tertiary');
// Outline ํ์
๋ฒํผ ์คํ ๋ฆฌ ์์ฑ
export const Outline: Story = createButtonStory('outline');
์ด๋ ๊ฒ ์ง์ ํด์ค ํ์ ๋ค๋ง๋ค story๊ฐ ์์ฑ๋๋ค.
๊ทธ๋ผ ์ด์ ํฌ๋ก๋งํฑ์ผ๋ก storybook์ ๋ฐฐํฌํด๋ณด๊ฒ ๋ค.
๋ฐฐํฌํ๋ ์ด์ ๋ ui๋ฅผ ๋ค๋ฅธ ํ์๋ค๊ณผ ๊ณต์ ํ์ฌ ์ฆ๊ฐ ํ์ธํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
๋จผ์ ํฌ๋ก๋งํฑ ํํ์ด์ง์ ๊ฐ์ github ๊ณ์ ์ ์ฐ๋ํ๊ณ , ๋ฐฐํฌํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ ํฌ๋ฅผ ์ ํํ์ฌ ํ๋ก์ ํธ์ ํ ํฐ์ ๋ฐ๊ธ๋ฐ๋๋ค!
๋ค์์ผ๋ก๋ ์๋ ๋ช ๋ น์ด๋ฅผ ์คํํด์ฃผ๋ฉด ๋๋ค.
pnpm add --save-dev chromatic
npx chromatic --project-token=<your-project-token>
project-token์ ํ๋ก์ ํธ์ ํ ํฐ์ ์ ๋ ฅ
๋ฐฐํฌ๋ ๋งํฌ๊ฐ cli์ ๋ธ, ๋ฐฐํฌ๋ ์์ฃผ ๊ฐ๋จํ๋ค.
๊ทธ๋ผ ์ด์ ์ด ๋ฐฐํฌ๋ฅผ ์๋ํํ์ฌ pr์ ์ฌ๋ฆด๋๋ง๋ค ์์ฑํ ์คํ ๋ฆฌ๊ฐ ๋ฐฐํฌ๋ ํฌ๋ก๋งํฑ์ ๋ฐ๋ก ๋ฐ์๋์ด ์ฆ๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์๊ฒ๋ ํด๋ณด๊ฒ ๋ค.
name: 'Chromatic Publish'
on:
pull_request:
branches:
- develop
permissions: write-all
jobs:
storybook:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install PNPM
run: npm i -g pnpm
- name: Cache node modules
id: cache-node
uses: actions/cache@v3
with:
path: |
**/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
if: steps.cache-node.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Build Storybook for Design System
run: pnpm --filter @confeti/design-system run build-storybook
- name: Publish to Chromatic
id: chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.AUTO_SYNC_CHROMATIC }}
token: ${{ secrets.SECRET_KEY }}
onlyChanged: true
autoAcceptChanges: true
- name: Comment PR
if: github.event_name == 'pull_request'
uses: thollander/actions-comment-pull-request@v2
env:
GITHUB_TOKEN: ${{ secrets.SECRET_KEY}}
with:
comment_tag: ${{github.event.number}}-storybook
message: '๐ดโโ ๏ธ Storybook ํ์ธ: ๐ ${{ steps.chromatic.outputs.storybookUrl }}'
edit_mode: update
github์์ ์๋์ผ๋ก ๊ด๋ฆฌํ๊ณ ์๋ GITHUB_TOKEN์ ๋ฐ๋ก ๋ฑ๋ก ์ํด์ค๋ ๋๋ค๊ณ ์๊ณ ์๋ค.
๊ทธ๋ฌ๋ ๋ค์๊ณผ ๊ฐ์ด workflow ๊ถํ๋ฌธ์ ๊ฐ ๊ณ์ ๋ฐ์ํ์ฌ, ๋ด ๊ฐ์ธ access token์ ๋ฐ๊ธํ์ฌ workflow ๊ถํ์ ์ถ๊ฐํ ๋ค์์ secret์ ๋ฑ๋ก ํ ๋์ ์ฌ์ฉํ์ฌ ํด๊ฒฐํ์๋ค..
Confeti์์๋ ๋์์ธ ์์คํ ์ ๋ชจ๋ ธ๋ ํฌ ๊ตฌ์กฐ๋ก ๊ด๋ฆฌํ๊ณ ์์ด, ๋์์ธ ์์คํ ๋ง ๋น๋ํ ์ ์๋๋ก ์ค์ ํ๋ ๋ฐ ์ด๋ ค์์ด ์์๋ค. ์ํฌํ๋ก์ฐ์ ๋ฐ๋ผ ๋ฃจํธ ๋๋ ํ ๋ฆฌ์์ storybook์ ๋น๋ํ์ง๋ง, package.json์์ ๋ช ๋ น์ด๋ฅผ ์ถ๊ฐํ์ง ์์์ ๋น๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
//๋ฃจํธ package.json์ ๋ค์ scripts ์ถ๊ฐ
"scripts": {
"build-storybook": "pnpm --filter @confeti/design-system run build-storybook"
},
๋น๋๋ฅผ ์ํด @confeti/design-system์ด ํฌํจ๋ package.json์์ name ํ๋๊ฐ ์ ๋๋ก ์ค์ ๋์ด ์์ด์ผ๋ง ํด๋น ํจํค์ง๊ฐ ํํฐ๋ง๋๋๋ฐ name์ ํ์ธ ์ํ๊ณ ๊ทธ๋ฅ ๊ฒฝ๋ก ๋๋ก packages/design-system์ผ๋ก ์ ๋ ฅํ์ฌ ๊ณ์ ์ค๋ฅ๋ฌ์๋ค.
-> name ํ์ธ ํ @confeti/design-system์ผ๋ก ๋ณ๊ฒฝ
- name: Build Storybook for Design System
run: pnpm --filter @confeti/design-system run build-storybook
๋ชจ๋
ธ๋ ํฌ์ ํด๋๊ตฌ์กฐ ์ดํด๊ฐ ์กฐ๊ธ ๋ถ์กฑํด์ ํค๋งธ์๋ ๊ฒ ๊ฐ๋ค.
๋ชจ๋
ธ๋ ํฌ์์๋ ๊ฐ ํจํค์ง๊ฐ ๋
๋ฆฝ์ ์ธ package.json์ ๊ฐ๊ณ ์์ผ๋ฏ๋ก, ๋ฃจํธ ๋๋ ํ ๋ฆฌ์ package.json๊ณผ ๊ฐ ํจํค์ง ๋๋ ํ ๋ฆฌ์ package.json์ ๊ตฌ๋ถํ์ฌ ๋ช
๋ น์ด๋ฅผ ์คํํด์ผ ํ๋ค !!
๋๋ถ์ ์คํ ๋ฆฌ๋ถ์์ ๋ฐ๋๋ผ์ต์คํธ๋ํธ ํ๋ฌ๊ทธ์ธ ์ค์น ์ ํ์ต๋๋น ใ ๊ฐ์ฌํฉ๋๋ค