๐์ฌ์ฌ์ฉ ๊ฐ๋ฅํ & ์ผ๊ด์ฑ ์๋ ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ
๋ฒํผ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ + ์ผ๊ด์ฑ ์๊ฒ ๋ง๋ค์ด๋ณด์.
1. ๋ชฉํ/์๊ตฌ์ฌํญ
1) ๋ฒํผ์ shape์ ํญ์ ๋์ผํด์ผ ํ๋ค.
๋ฒํผ์ ์ ์ฒด์ ์ธ ๋ชจ์์ ๋ณํ์ง ์์์ผ ํ๋ค. ์ฆ, ์ด๋ค ํฌ๊ธฐ๋ฅผ ๊ฐ๋ , ์ด๋ค ์์ ๊ฐ๋์ง ๊ฐ์ border-radius, padding ๊ฐ์ ๋ํ ์์ฑ๋ค์ ๊ณ ์ ๋์ด ์์ด์ผ ํ๋ค.
2) variant props์ ํตํด preset๋ ์์์ ์ ํํ ์ ์์ด์ผ ํ๋ค.
์ผ๊ด์ฑ ์๋ ๋์์ธ์ ๊ตฌํํ๊ธฐ ์ํด ์ ํ์ ์ผ๋ก props๋ฅผ ํตํด ์ ์๋ ์คํ์ผ์ ์ ์ฉํ ์ ์๋๋ก ๋ง๋ ๋ค.
๋ฏธ๋ฆฌ ์ ์๋ ์์ variant๋ผ๋ props์ ์ด์ฉํ์ฌ ์ ํํ ์ ์๋๋ก ๋ง๋ ๋ค.
3) size props๋ฅผ ํตํด preset๋ ํฌ๊ธฐ๋ฅผ ์ ํํ ์ ์์ด์ผ ํ๋ค.
์์๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก size ์ญ์ ์ผ๊ด์ฑ ์๋ ๋์์ธ์ ๊ตฌํํ๊ธฐ ์ํด, ๋ฏธ๋ฆฌ ์ ์๋ ์คํ์ผ์ size๋ผ๋ props์ ์ด์ฉํ์ฌ ์ ํํ ์ ์๋๋ก ํ๋ค.
4) ๋ฒํผ hover, active, disabled์ ๋ํ ๊ฐ๊ฐ์ ์คํ์ผ์ด ์กด์ฌํ๋ฉฐ, ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ํด๋ฆญํ์ ๋ ํด๋ฆญํ๋ ๋๋์ด ๋ค์ด์ผ ํ๋ค. disabled ๋์์ ๋๋ disabled ๋์๋ค๋ ๋๋์ด ๋ค์ด์ผ ํ๋ค.
๋ฐ๋ผ์ variant์ ๋ง๋๋ก ์ ์ ํ ์์์ :hover, :active, :diabled ๋ฑ ๋ค์ํ ๊ฒฝ์ฐ์ ๋ง๊ฒ ๋ฐฐ์นํด์ผ ํ๋ค.
์ฌ์ฌ์ฉ์ฑ์ ๋์ด๊ธฐ ์ํด์๋ ํด๋น ์ปดํฌ๋ํธ๊ฐ ๋๋ฉ์ธ์ ์ฝํ์ง ์์์ผ ํ๋ฉฐ, props, attributes์ ์ด๋ฆ๋ค์ด ์ผ๋ฐ์ ์ด์ด์ผ ํ๋ค.
2. ์ฝ๋๋ก ์ดํด
React.js + styled-components๋ฅผ ์ด์ฉํด์ ๋ง๋ค์ด๋ณด์๋ค.
*์ ์ฒด ButtonBase ์ฝ๋
import React from 'react';
import styled, { css } from "styled-components";
import { get500Color, get600Color, get700Color, get300Color } from '../../utils/color';
import { DEFAULT_FONT_SIZES } from "../../utils/font";
// ๋ฒํผ ์คํ์ผ ์ ์
const buttonRoleStyle = css`
${({ variant = 'default', disabled }) => css`
background-color: ${get500Color(variant)};
color: ${get500Color()};
&:hover {
background-color: ${get600Color(variant)};
}
&:active {
background-color: ${get700Color(variant)};
}
&:disabled {
background-color: ${get300Color(variant)};
pointer-events: none;
cursor: ${disabled ? 'default' : 'pointer'};
}
`}
`;
const sizeStyle = css`
${({ size = 'md' }) => {
if (size === 'sm') {
return css`
padding: 8px 10px;
font-size: ${DEFAULT_FONT_SIZES.b2}px;
`;
}
if (size === 'lg') {
return css`
padding: 12px 48px;
font-size: ${DEFAULT_FONT_SIZES.b2}px;
`;
}
if (size === 'xl') {
return css`
padding: 12px 60px;
font-size: ${DEFAULT_FONT_SIZES.b1}px;
`;
}
// ๊ธฐ๋ณธ์ ์ผ๋ก 'md'์ผ ๋์ ์คํ์ผ (default)
return css`
padding: 10px 12px;
font-size: ${DEFAULT_FONT_SIZES.b2}px;
`;
}}
`;
const ButtonBase = styled.button`
display: inline-flex;
gap: 4px;
justify-content: center;
align-items: center;
vertical-align: center;
position: relative;
min-width: 64px;
border: none;
border-radius: 6px;
padding: 10px 12px;
cursor: pointer;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* button์ Content๋ฅผ ์ ํํ ์ ์๋๋ก ํ๋ค.*/
user-select: none;
transition: background-color 0.1s ease;
/* ๋ฐฐ๊ฒฝ์ ์คํ์ผ ์ ์ฉ */
${buttonRoleStyle}
/* ์ฌ์ด์ฆ ์คํ์ผ ์ ์ฉ */
${sizeStyle}
`;
function Button(props) {
return <div>
<ButtonBase variant={props.variant} size={props.size} disabled={props.disabled}>{props.label}</ButtonBase>
</div>;
}
export default Button;
⇒๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉ
import Button from "./components/common/ButtonBase";
function App() {
return (
<div>
<Button label="๊ธฐ๋ณธ๋ฒํผ" variant="primary" size="lg"></Button>
<Button label="์๋ฌ๋ฒํผ" variant="error"></Button>
<Button label="๋นํ์ฑํ๋ฒํผ" variant="warning" disabled="disabled"></Button>
</div>
);
}
export default App;
3. Storybook
Storybook์ด๋ UI ์ปดํฌ๋ํธ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฐ๋ฐํ ์ ์๊ฒ ํ๋ ๋๊ตฌ์ด๋ค.
๐งReact์ Storybook ์ ์ฉ๋ฐฉ๋ฒ
1๋จ๊ณ: Storybook ์ค์น
React ํ๋ก์ ํธ์ ๋ฃจํธ ๋๋ ํฐ๋ฆฌ์์ ์๋ ๋ช ๋ น์ด๋ฅผ ์คํํ์ฌ Storybook์ ์ค์นํ๋ค.
npm install -g @storybook/cli
getstorybook -V //๋ฒ์ ํ์ธ
getstorybook init
2๋จ๊ณ: ์คํฌ๋ฆฝํธ ์ค์
package.json ํ์ผ์ ์ด๊ณ , ๋ค์๊ณผ ๊ฐ์ด scripts ์น์ ์ Storybook์ ์์ํ ์ ์๋ ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํ๋ค.
"scripts": {
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
}
3๋จ๊ณ: Storybook ๊ตฌ์ฑํ๊ธฐ
.storybook ํด๋ ์์ main.js ํ์ผ์ ํตํด Storybook์ ๊ตฌ์ฑํ ์ ์๋ค. ์ด ํ์ผ์์ ํ๋ก์ ํธ์ ์คํ ๋ฆฌ ํ์ผ๋ค์ ์ฐพ์ ์ ์๋ ๊ฒฝ๋ก๋ฅผ ์ค์ ํ๋ค.
javascript์ฝ๋ ๋ณต์ฌ
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
};
4๋จ๊ณ: ์คํ ๋ฆฌ ์์ฑํ๊ธฐ
์ปดํฌ๋ํธ ๋ณ๋ก ์คํ ๋ฆฌ ํ์ผ์ ์์ฑํ๋ค. ์๋ฅผ ๋ค์ด, Button ์ปดํฌ๋ํธ์ ๋ํ ์คํ ๋ฆฌ๋ฅผ ์์ฑํ๋ ค๋ฉด ButtonBase.stories.jsx ํ์ผ์ ์์ฑํ๊ณ ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ค.
์์น๋ ๋ค์๊ณผ ๊ฐ๋ค.
// src/ButtonBase.stories.js
import React from "react";
import Button from "../components/common/ButtonBase";
export default {
title: "Components/Button",
component: Button,
argTypes: {
variant: {
control: {
type: "select",
options: ["default", "primary", "secondary"], // variant ์ต์
์ ๋ง๊ฒ ์์
},
},
size: {
control: {
type: "select",
options: ["sm", "md", "lg", "xl"],
},
},
disabled: {
control: "boolean",
},
label: {
control: "text",
},
},
};
const Template = (args) => <Button {...args} />;
export const Default = Template.bind({});
Default.args = {
variant: "default",
size: "md",
disabled: false,
label: "Button",
};
export const Primary = Template.bind({});
Primary.args = {
variant: "primary",
size: "md",
disabled: false,
label: "Primary Button",
};
export const Large = Template.bind({});
Large.args = {
variant: "error",
size: "lg",
disabled: false,
label: "Large ERROR Button",
};
export const Disabled = Template.bind({});
Disabled.args = {
variant: "default",
size: "md",
disabled: true,
label: "Disabled Button",
};
*Storybook ๋ฌธ๋ฒ์ ๋ค์๊ณผ ๊ฐ๋ค.
1) export default ๊ฐ์ฒด
: Stroybook์์์ ์ปดํฌ๋ํธ ์ค์ ์ ์
- title: Storybook ์ฌ์ด๋๋ฐ์์ ์ปดํฌ๋ํธ๊ฐ ์ด๋ป๊ฒ ํ์๋ ์ง ์ ์
- component: ์ด๋ค ์ปดํฌ๋ํธ๋ฅผ ๋ฌธ์ํํ ์ง ์ง์
- argTypes์ ํํ ์ ์๋ ์ต์ ๋ค์ “select” ์ปจํธ๋กค๋ก ์ ๊ณตboolean/text ํ์ ์ผ๋ก ํ์
- disabled: { control: "boolean", }, label: { control: "text", },
- variant: { control: { type: "select", options: ["default", "primary", "secondary"], // variant ์ต์ ์ ๋ง๊ฒ ์์ }, },
2) Template ํจ์
: ์ค์ ๋ก ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ ๋ ์ฌ์ฉํ ํ ํ๋ฆฟ ํจ์
const Template = (args) => <Button {...args} />;
3) Story ์ ์
: Template.bind({})๋ฅผ ํตํด ๊ธฐ๋ณธ ํ ํ๋ฆฟ ํจ์๋ฅผ ๋ณต์ ํ๊ณ , ๊ฐ ์คํ ๋ฆฌ๋ณ๋ก ์ด๊ธฐ ์ํ๋ฅผ ์ค์ ํ๋ค.
- Default, Primary, Large, Disabled ๋ฑ ๊ฐ๊ฐ์ ์คํ ๋ฆฌ๋ ๋ค๋ฅธ args ๊ฐ์ ๊ฐ์ง๋๋ค.
export const Primary = Template.bind({});
Primary.args = {
variant: "primary",
size: "md",
disabled: false,
label: "Primary Button",
};
5๋จ๊ณ: Storybook ์คํํ๊ธฐ
๋ชจ๋ ์ค์ ์ ๋ง์น ํ, ์๋ ๋ช ๋ น์ด๋ก Storybook์ ์คํํ ์ ์๋ค.
npm run storybook
์ด ๋ช ๋ น์ด๋ฅผ ์คํํ๋ฉด ๋ธ๋ผ์ฐ์ ์์ Storybook์ด ์์๋๋ฉฐ, ๊ตฌ์ฑํ ์คํ ๋ฆฌ๋ค์ ๋ณผ ์ ์๋ค.
์ฐธ๊ณ
[React] ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ ๋ง๋ค์ด ์ฌ์ฉํ๊ธฐ
์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฒํผ ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ - React
React StoryBook์ ๊ธฐ์ด(์คํ ๋ฆฌ ์์ฑ / ์คํ ๋ฆฌ๋ถ ์คํ)
'๐ป์น(Web) > React' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[React]Custom Hook (0) | 2024.06.28 |
---|---|
[React]Vite, Redux (0) | 2024.06.27 |
[React]AWS EC2๋ก React ๋ฐฐํฌํ๊ธฐ, ๋๋ฉ์ธ ์ฐ๊ฒฐ (0) | 2023.09.07 |
[React]Firebase๋ก React ๋ฐฐํฌํ๊ธฐ, Mixed-Content ์๋ฌ (1) | 2023.09.03 |
[React]Netlify๋ก React ๋ฐฐํฌํ๊ธฐ: ์๋ก๊ณ ์นจ, API ์ฐ๊ฒฐ ์๋ฌ ํด๊ฒฐ๋ฐฉ๋ฒ (0) | 2023.09.01 |