기존에 공용 Button 컴포넌트가 있으나 다음과 같은 요구사항을 팀원에게 전달받아 리팩토링을 진행했습니다.
button
요소의 모든 속성을 props로 전달할 수 있도록 확장다음은 기존 코드의 일부입니다.
interface ButtonProps {
type: 'button' | 'submit';
pattern: 'box' | 'round';
size: 'sm' | 'md' | 'lg' | 'xl';
variant?: 'active' | 'highlight' | 'line';
fullWidth?: boolean;
theme?: Theme;
onClick?: () => void;
children: ReactNode;
disabled?: boolean;
}
export const Button = ({
type,
pattern,
size,
variant,
fullWidth,
onClick,
children,
disabled,
}: ButtonProps) => {
return (
<ButtonLayout
type={type}
pattern={pattern}
size={size}
variant={variant}
fullWidth={fullWidth}
onClick={onClick}
disabled={disabled}
>
{children}
</ButtonLayout>
);
};
button
요소의 기본적인 속성까지 props로 전달받아 ButtonProps 인터페이스가 복잡합니다.
ComponentProps를 적용하여 ButtonProps 인터페이스에서 button
요소의 기본적인 속성을 제거합니다.
또한, 나머지 매개변수를 이용하여 button
요소의 기본적인 속성을 전달받을 수 있도록 합니다.
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
fullWidth?: boolean;
shape?: 'box' | 'round';
size?: 'sm' | 'md' | 'lg' | 'xl';
text: string;
variant?: 'active' | 'highlight' | 'line' | 'disabled';
}
export const Button = ({
fullWidth = false,
shape = 'box',
size = 'lg',
text,
variant = 'active',
...props
}: ButtonProps) => {
return (
<ButtonLayout
shape={shape}
size={size}
variant={variant}
fullWidth={fullWidth}
{...props}
>
{text}
</ButtonLayout>
);
};
기존의 스타일 코드는 다음과 같이 각 props에 따라 조건문을 통해 스타일이 적용되도록 구현되어 있었습니다.
const patternStyles = ({ pattern }: ButtonProps) => css`
${pattern === 'box' && css`
border-radius: 6px;
`}
${pattern === 'round' && css`
border-radius: 100px;
`}
`;
const sizeStyles = ({ size }: ButtonProps) => css`
${size === 'sm' && css`
// sm styles
`}
${size === 'md' && css`
// md styles
`}
${size === 'lg' && css`
// lg styles
`}
${size === 'xl' && css`
// xl styles
`}
`;
const variantStyles = ({ variant }: ButtonProps) => css`
// variant styles
`;
const ButtonLayout = styled.button<ButtonProps>`
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'fit-content')};
background: ${({ theme }) => theme.colors.primary_00};
color: ${({ theme }) => theme.colors.white};
user-select: none;
${patternStyles}
${sizeStyles}
${variantStyles}
&:disabled {
background: ${({ theme }) => theme.colors.gray_04};
color: ${({ theme }) => theme.colors.white};
}
`
props에 대한 조건문이 반복되었고, 이로인해 가독성이 떨어져 보였습니다.
Button 컴포넌트를 리팩토링하면서 참고한 코드가 매우 깔끔해 보여서 그와 유사하게 적용하고자 emotion의 css funtion을 사용했습니다.
참고했던 코드 예시
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> { size?: 'medium' | 'large'; } export function Button({ size = 'medium', ...props }: Props) { return ( <button css={{ border: '0 solid transparent', borderRadius: '7px', cursor: 'pointer', lineHeight: '1.4', ...SIZE_VARIANTS[size], }} {...props} /> ); } const SIZE_VARIANTS = { medium: { fontSize: '15px', fontWeight: 400, padding: '11px 16px', }, large: { fontSize: '17px', fontWeight: 600, padding: '11px 22px', }, };
다음과 같이 emotion css funtion을 적용할 수 있습니다.
import { css } from '@emotion/react';
export const Button = ({
fullWidth = false,
shape = 'box',
size = 'lg',
style,
text,
variant = 'active',
...props
}: ButtonProps) => {
return (
<button
css={css`
width: ${fullWidth ? '100%' : 'fit-content'};
background-color: ${theme.colors.primary_00};
color: ${theme.colors.white};
user-select: none;
`}
{...props}
>
{text}
</button>
);
};
이때 다음과 같은 에러가 발생했습니다.
이는 HTMLButtonElement
에 css
속성이 존재하지 않아 발생한 타입 에러입니다.
DetailedHTMLProps
는 React의 HTML 속성과 이벤트를 모두 포함하는 인터페이스이고 HTMLButtonElement
는 button
요소의 HTML 태그 인터페이스로, 기본적으로 css
속성이 존재하지 않습니다.
따라서, DetailedHTMLProps
인터페이스에 css
속성을 추가하여 emotion의 css prop을 사용할 수 있도록 해야합니다.
Emotion 공식문서에 따르면 css prop을 TypeScript와 사용하기 위해 다음의 compilerOptions
를 TSConfig에 추가해야 합니다.
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@emotion/react"
}
}
하지만 다음의 에러가 남아있습니다.
이 에러는 eslint rule에 다음의 규칙을 추가하여 해결할 수 있습니다.
// .eslintrc.json
{
"rules": {
"react/no-unknown-property": ["error", { "ignore": ["css"] }]
}
}
import { css } from '@emotion/react';
import type { ButtonHTMLAttributes } from 'react';
import { theme } from 'styles';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
fullWidth?: boolean;
shape?: 'box' | 'round';
size?: 'sm' | 'md' | 'lg' | 'xl';
text: string;
variant?: 'active' | 'highlight' | 'line' | 'disabled';
}
export const Button = ({
fullWidth = false,
shape = 'box',
size = 'lg',
text,
variant = 'active',
...props
}: ButtonProps) => {
return (
<button
css={css`
width: ${fullWidth ? '100%' : 'fit-content'};
background-color: ${theme.colors.primary_00};
color: ${theme.colors.white};
user-select: none;
${SHAPE_STYLES[shape]}
${SIZE_STYLES[size]}
${VARIANT_STYLES[variant]}
&:disabled {
${VARIANT_STYLES.disabled}
}
`}
{...props}
>
{text}
</button>
);
};
const SHAPE_STYLES = {
box: css`
border-radius: 6px;
`,
round: css`
border-radius: 100px;
`,
};
const SIZE_STYLES = {
sm: css`
// sm styles
`,
md: css`
// md styles
`,
lg: css`
// lg styles
`,
xl: css`
// xl styles
`,
};
const VARIANT_STYLES = {
// variant styles
};
참고했던 코드는 style 객체 자체를 css prop에 넘겨주었지만 저는 css funtion을 넘겼습니다.
그 이유는 각 스타일 객체(SHAPE_STYLES
, SIZE_STYLES
, VARIANT_STYLES
)의 값을 css function으로 정의했기 때문입니다.
일반 객체로 정의하면 css 자동완성 기능이 동작하지 않습니다.
css function으로 정의하면 자동완성 기능을 이용해서 스타일 코드 작성이 편리하고 이를 통해 오타를 방지할 수 있기 때문에 이 방법으로 코드를 작성했습니다.
참고