우리만의 디자인 시스템을 만들고 싶다
새롭게 입점사 프로젝트에 들어가며
디자인시스템대로 컴포넌트를 만들어 회사 내 다른 프로젝트에서도 사용하고 싶었다!
지금까지는 디자인 시스템이 정해져 있는 것이 아니라 그때그때 시안에 맞춰서 작업해서 소스가 더러웠다
이번 기회에 우리만의 디자인 시스템을 만들고자 했다
위처럼
버튼에 대한 디자인 시스템이 정의되어 있다면
색상, 사이즈에 따라 버튼 컴포넌트를 만들면 된다!
Button Component 만들기
어떻게 만들면 좋을까에 대해서 고민하며 찾아보다가 velog의 오픈소스를 봤는데 size, color를 따로 정의하고 props로 받는 부분이 너무 깔끔하게 잘되어 있어 이를 참고하여 만들었다
사이즈 스타일링
피그마에 정의된 대로 사이즈를 나누어 각각의 사이즈를 상수로 정의해 줬다
(이렇게 정의하면 모두가 같은 사이즈 규칙에 따라 사용할 수 있고 변경 시에도 이 파일만 변경해 두면 된다 👍)
src/styles/theme/button.ts파일을 만들고 아래 소스를 추가했다
export const buttonSizeMap: {
[size: string]: {
height: string
fontSize: string
fontWeight: string
borderRadius: string
padding: string
}
} = {
xs: {
height: '29px',
fontSize: '12px',
fontWeight: '500',
borderRadius: '10px',
padding: '6px 14px',
},
s: {
height: '37px',
fontSize: '12px',
fontWeight: '500',
borderRadius: '12px',
padding: '10px 14px',
},
m: {
height: '40px',
fontSize: '14px',
fontWeight: '500',
borderRadius: '14px',
padding: '10px 14px',
},
l: {
height: '48px',
fontSize: '14px',
fontWeight: '700',
borderRadius: '16px',
padding: '14px 18px',
},
xl: {
height: '52px',
fontSize: '16px',
fontWeight: '700',
borderRadius: '16px',
padding: '14px 18px',
},
'2xl': {
height: '56px',
fontSize: '18px',
fontWeight: '700',
borderRadius: '16px',
padding: '15px 20px',
},
'3xl': {
height: '72px',
fontSize: '22px',
fontWeight: '700',
borderRadius: '20px',
padding: '20px 24px',
},
}
색상 스타일링
색상은 styled-components를 사용하고 있어 theme에 정의된 색상을 가져왔고
src/styles/theme/button.ts에 색상에 대한 규칙을 추가해 줬다
export const buttonColorMap: {
[color: string]: {
background: string
color: string
border?: string
}
} = {
primaryBlue: {
background: theme.colors.primaryBlue,
color: theme.colors.primaryWhite,
},
primaryBlack: {
background: theme.colors.primaryBlack,
color: theme.colors.primaryWhite,
},
secondaryBlack: {
background: theme.colors.secondaryGray1,
color: theme.colors.primaryWhite,
},
secondaryGray: {
background: theme.colors.secondaryGray4,
color: theme.colors.secondaryGray1,
},
tertiary: {
background: theme.colors.secondaryGray5,
color: theme.colors.secondaryGray2,
},
outlineBlue: {
background: theme.colors.primaryWhite,
color: theme.colors.primaryBlue,
border: `1px solid ${theme.colors.primaryBlue}`,
},
outlineBlack: {
background: theme.colors.primaryWhite,
color: theme.colors.secondaryGray1,
border: `1px solid ${theme.colors.secondaryGray1}`,
},
outlineGray: {
background: theme.colors.primaryWhite,
color: theme.colors.secondaryGray2,
border: `1px solid ${theme.colors.secondaryGray2}`,
},
}
Button Component props
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'size'> {
isLoading?: boolean
color: ColorType
size: SizeType
children: ReactNode
}
Button Component는 isLoading, color, size, children을 props로 받는다
그 외에 버튼에 대한 기본적인 속성들은 ButtonHTMLAttributes<HTMLButtonElement>에서 상속받고 size는 제외시켰다
isLoading이 필요했던 이유는
버튼을 클릭하고 api요청을 하는데 응답까지 오랜 시간이 걸리는 경우!
로딩 UI가 없으면 사용자는 api 요청 중이라는 사실을 인지하지 못해 왜 반응이 없지? 하며 당황할 것이고
이로 인해 계속해서 버튼을 클릭하게 되는 경우 api 요청은 여러 번 이루어질 것이다 😅
이를 막기 위해 버튼에 로딩 UI를 추가해 줬다
로딩 UI가 보이는 경우(isLoading이 true인 경우)에는 클릭이 안되도록 css에서 처리했다
${(props) =>
props.isLoading &&
`
@keyframes spinner-border {
to {
transform: rotate(360deg);
}
}
color: transparent !important;
position: relative;
background-image: none !important;
pointer-events: none !important;
&::after {
color: ${buttonColorMap[props.color].color};
content: "";
display: inline-block;
width: 1.2em;
height: 1.2em;
vertical-align: text-bottom;
border: 3px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
-webkit-animation: spinner-border 0.75s linear infinite;
animation: spinner-border 0.75s linear infinite;
opacity: 1 !important;
position: absolute;
}
`};
전체 소스
Button component
import { ButtonHTMLAttributes, ReactNode } from 'react'
import { buttonColorMap, buttonSizeMap } from 'src/styles/theme/button'
import styled from 'styled-components'
interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'size'> {
isLoading?: boolean
color: ColorType
size: SizeType
children: ReactNode
}
export type ColorType =
| 'primaryBlue'
| 'primaryBlack'
| 'secondaryBlack'
| 'secondaryGray'
| 'tertiary'
| 'outlineBlue'
| 'outlineBlack'
| 'outlineGray'
type SizeType = 'xs' | 's' | 'm' | 'l' | 'xl' | '2xl' | '3xl'
const Button = ({ color, size, children, isLoading, ...rest }: ButtonProps) => {
return (
<ButtonStyled color={color} size={size} isLoading={isLoading} {...rest}>
{children}
</ButtonStyled>
)
}
const ButtonStyled = styled.button<{
isLoading?: boolean
color: ColorType
size: SizeType
}>`
display: flex;
align-items: center;
justify-content: center;
background: ${(props) => buttonColorMap[props.color].background};
color: ${(props) => buttonColorMap[props.color].color};
border: ${(props) => buttonColorMap[props.color].border};
font-size: ${(props) => buttonSizeMap[props.size].fontSize};
font-weight: ${(props) => buttonSizeMap[props.size].fontWeight};
border-radius: ${(props) => buttonSizeMap[props.size].borderRadius};
padding: ${(props) => buttonSizeMap[props.size].padding};
height: ${(props) => buttonSizeMap[props.size].height};
&:disabled {
cursor: not-allowed;
background: ${({ theme }) => theme.colors.secondaryGray5};
color: ${({ theme }) => theme.colors.secondaryGray3};
}
${(props) =>
props.isLoading &&
`
@keyframes spinner-border {
to {
transform: rotate(360deg);
}
}
color: transparent !important;
position: relative;
background-image: none !important;
pointer-events: none !important;
&::after {
color: ${buttonColorMap[props.color].color};
content: "";
display: inline-block;
width: 1.2em;
height: 1.2em;
vertical-align: text-bottom;
border: 3px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
-webkit-animation: spinner-border 0.75s linear infinite;
animation: spinner-border 0.75s linear infinite;
opacity: 1 !important;
position: absolute;
}
`};
`
export default Button
실제 Button Component 사용 시
<Button type="button" onClick={onConfirm} size="xl" color="primaryBlue">
확인
</Button>
<Button type="button" onClick={onCancel} size="xl" color="secondaryGray">
취소
</Button>
결과화면(Storybook)
참고자료 - https://github.com/velopert/velog-client/blob/master/src/components/common/Button.tsx
'IT > React' 카테고리의 다른 글
필터 구현 로직 custom hook으로 만들기(+뒤로가기 시 필터값 유지) (0) | 2023.12.05 |
---|---|
Pagination custom hook (0) | 2023.11.06 |
Pagination 구현하기 (0) | 2023.10.17 |
React cdn 방식으로 시작하기 (php파일에서 리액트 사용하기) (0) | 2022.06.02 |
듀얼 셀럭터 (0) | 2022.04.15 |