'DetailedHTMLProps<ButtonHTMLAttributes, HTMLButtonElement>' 형식에 'css' 속성이 없습니다. | Emotion with TypeScript

Bori·2024년 2월 11일
1

어쨌든 공부

목록 보기
39/41

공용 Button 컴포넌트 만들기

기존에 공용 Button 컴포넌트가 있으나 다음과 같은 요구사항을 팀원에게 전달받아 리팩토링을 진행했습니다.

  • button 요소의 모든 속성을 props로 전달할 수 있도록 확장
  • 현재 적용된 스타일 이외의 스타일을 유연하게 적용할 수 있도록 수정

ComponentProps 적용하기

다음은 기존 코드의 일부입니다.

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>
  );
};

Emotion css funtion 사용하기

기존의 스타일 코드는 다음과 같이 각 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',
  },
};

'DetailedHTMLProps<ButtonHTMLAttributes, HTMLButtonElement>' 형식에 'css' 속성이 없습니다.

다음과 같이 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>
  );
};

이때 다음과 같은 에러가 발생했습니다.

이는 HTMLButtonElementcss 속성이 존재하지 않아 발생한 타입 에러입니다.
DetailedHTMLProps는 React의 HTML 속성과 이벤트를 모두 포함하는 인터페이스이고 HTMLButtonElementbutton 요소의 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으로 정의하면 자동완성 기능을 이용해서 스타일 코드 작성이 편리하고 이를 통해 오타를 방지할 수 있기 때문에 이 방법으로 코드를 작성했습니다.

참고

0개의 댓글