아침에 일어난 나를 반겨주는 것은 타입스크립트에 대한 내용이었습니다.
해당 코드는 다음과 같습니다.
HTMLAttributes
는 평소에 자주 사용하던 속성을 넘겨주는 부분이었는데 좋지 않다고 얘기를 하고있습니다. 어느 부분에서 문제인지 파악을 해봐야겠습니다.
아래부터 무엇이 문제인지 또 왜 위로갈수록 좋은 패턴인지에 대해서 정리해봅니다.
흔히 사용하던 HTMLAttributes
부터 확인을 들어가봅니다.
HTMLAttributes
의 생김새는 다음과 같습니다.
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
// React-specific Attributes
defaultChecked?: boolean | undefined;
defaultValue?: string | number | readonly string[] | undefined;
suppressContentEditableWarning?: boolean | undefined;
// ...
itemRef?: string | undefined;
results?: number | undefined;
security?: string | undefined;
unselectable?: "on" | "off" | undefined;
inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;
is?: string | undefined;
}
HTML
태그를 만든다고 했을 때 사용할 수 있는 모든 속성들이 들어가 있습니다.
🧐 근데 괜히 궁금증이 생깁니다.. 과연 T
에는 무슨 타입이 들어갈 수 있을까?
타고타고 들어가면 결론에 도달합니다.
interface DOMAttributes<T> {
children?: ReactNode | undefined;
dangerouslySetInnerHTML?: {
// Should be InnerHTML['innerHTML'].
// But unfortunately we're mixing renderer-specific type declarations.
__html: string | TrustedHTML;
} | undefined;
// Clipboard Events
onCopy?: ClipboardEventHandler<T> | undefined;
onCopyCapture?: ClipboardEventHandler<T> | undefined;
onCut?: ClipboardEventHandler<T> | undefined;
onCutCapture?: ClipboardEventHandler<T> | undefined;
onPaste?: ClipboardEventHandler<T> | undefined;
onPasteCapture?: ClipboardEventHandler<T> | undefined;
/// ...
}
전부 optional인 타입 밭이 보입니다. 때문에 any
타입과 같이 어떤 타입을 주더라도 다 받아주는 겁니다.
그럼 왜 💩 이라는 평가를 받는지 생각을 해봅니다.
UserType
과 같이 정해져있지않은 모든 타입이 들어갈 수 있습니다. 해당 타입은 any
타입으로 어떤 값을 넣어도 다 받아줍니다.any
타입이다 보니 자동 완성을 제공하거나 정확한 타입 힌트를 제공하기가 어렵습니다. 제가 만약 커스텀 버튼 컴포넌트를 만든다고 해보겠습니다.
type PropsType = {
onClick: () => void
} & React.HTMLAttributes<HTMLButtonElement>
const MainButton = (props:PropsType) => {
return (
<button {...props}>
{// ...}
</button>
)
}
버튼의 타입을 본다면 any
타입이기 때문에 모든 타입의 속성들을 받을 수 있습니다. 하지만 불필요한 모든 HTML 속성들까지 담고 있는 것을 볼 수 있습니다. 이는 인터페이스를 명확히 하기 위해서는 불필요한 타입까지 보여지는게 아닐까? 라는 생각이 듭니다.
두번째 타입인 ComponentProps
타입을 보겠습니다.
type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = T extends
JSXElementConstructor<infer P> ? P
: T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T]
: {};
T가 JSXElementConstructor<infer P>
타입인 경우: infer
키워드를 사용하여 제네릭 타입 P를 추론합니다.
추론된 P 타입은 해당 컴포넌트의 속성 타입을 나타냅니다.
예를 들어, T가 React.FC와 같은 함수 컴포넌트 생성자인 경우, P는 Props 타입이 됩니다.
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
},
)
type ButtonType = React.ComponentProps<typeof Button>;
// ButtonType: ButtonProps & React.RefAttributes<HTMLButtonElement>
T가 JSX.IntrinsicElements
의 키인 경우: JSX.IntrinsicElements
는 React의 내장 요소(Element) 타입들을 포함하는 인터페이스입니다. T가 이러한 내장 요소의 키인 경우, JSX.IntrinsicElements[T]
를 반환합니다. 예를 들어, T가 div
인 경우, JSX.IntrinsicElements['div']
는 div
요소의 속성 타입을 반환한다.
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
},
)
type ButtonType = React.ComponentProps<"div">
// ButtonType: React.ClassAttributes<HTMLDivElement> & React.HTMLAttributes<HTMLDivElement>
1
을 활용하는 방법은 다음과 같습니다. 컴포넌트 만으로 Props
의 타입을 가져올 수 있다.
type Props = {
textList: string[]
duration: number
}
export default function SlotMachine({ textList, duration }: Props) {
// ...
}
type SlotType = ComponentProps<typeof SlotMachine>
다음과 같이 작성이 되어있는 컴포넌트의 Props
타입을 가져오기 위해서 아래와 같이 작성하면 타입을 가져올 수 있는 것입니다.
type Props = {
user_name: string
user_gender: string
user_age: number
user_phone: string
user_date: string
user_hobby: string
}
export function UserList(props: PropsWithChildren<Props>) {
return (
<ul>
{// ...}
</ul>
)
}
// UserList 컴포넌트의 타입을 재사용 할 수 있습니다.
export function OneUser(props:ComponentProps<typeof UserList>){
return (
<li>
{// ...}
</li>
)
}
이전에는 ComponentProps
타입을 모를 경우에는 매번 타입도 같이 내보내주면서 했었지만 이제 컴포넌트 만으로도 타입을 사용 가능한 것을 알았습니다.
2
를 활용하는 방법은 다음과 같습니다.
ComponentProps
로 속성을 가져올 경우 HTML에 있는 기본 속성들만 보여주는 것이 아닌 button
에는 className, style, onClick 등과 같은 일반적인 HTML 속성들만 보여줍니다.
즉 ComponentProps<'button'>
에는 button에만 사용이 되는 속성이 들어간다고 생각하면 됩니다. 반면 이전 HtmlAttributes
는 해당 속성들도 포함하지만 HTML 요소에 대한 모든 attributes
의 자동완성을 포함하기 때문에 불필요한 자동완성이 많습니다. 때문에 컴포넌트 간의 인터페이스를 명확히 하고, 다른 개발자와 협업할 때는 ComponentProps<T>
를 사용할 경우 불필요한 attributes
를 줄일 수 있어 효율적으로 사용할 수 있습니다.
ComponentPropsWithoutRef
는 이름에서부터 알 수 있듯이 Ref
속성 만을 받지 않는다고 생각을 하면 됩니다.
하지만 사실 타입의 이름에서 알 수 있듯이 Ref를 받지 않는다고 하지만 실제로는
ComponentProps
와 똑같이 작동합니다.
협력을 할 경우 구성 요소의 참조가 전달되는지 여부를 명시적으로 지정하는 것을 선호할 수 있습니다
때문에 ComponentPropsWithoutRef
를 Perfect하다고 얘기를 한 것 같습니다.
만약 ref와 같이 사용하고 싶다고 한다면 ComponentPropsWithRef
를 사용하면 됩니다. 사용법은 ComponentProps
와 동일합니다.
불필요한
props
까지 넘기지 말고 실제로 사용하는props
만 추출해서 넘기자.ComponentPropsWithoutRef
는 타입 이름에서 알 수 있다시피 구성 요소의 참조가 없다는 것까지 알려주기에 좋다고 하는 것이다.