오랜만에 블로그 글을 작성한다.
여기저기 이래저래 바쁘기도 했고, 마땅히 쓸 내용이 없기도 했는데 typescript을 react에서 어떻게하면 좀 더 잘 사용할 수 있을까 고민하던 중 알게된 내용을 정리하려한다.
React로 개발을 하다보면 UI를 위해 많은 요소들을 따로 컴포넌트로 분리해 프로젝트에 맞게 스타일링과 여러 작업을 하고 사용하는 경우가 많다.
관심사 분리도 하고, 가독성도 높이고, 이래저래 여하튼 좋다.
가령 Button을 사용하는 경우를 보자 기본적인 버튼의 기능을 가지고 거기에 앱의 스타일을 입한다고 하자
간단하게 구현하면 다음처럼 할 수 있다.
import React from "react";
import styles from "./Button.module.css";
interface ButtonProps extends
function Button({
variant = "primary",
size = "md",
children,
...otherProps
}) {
return (
<button
className={`variant와 size 값에 맞는 className`}
{...otherProps}
>
{children}
</button>
);
}
export default Button;
css 스타일링을 위해서 위예제는 css module를 사용했지만 원하는 라이브러리를 사용하면 되겠다.
이렇게 구현을 하고 사용을 하면
어떤 컴포넌트에서든 같은 스타일이 적용된 button을 사용할 수 있다.
여기에 typescript를 입히면? 조금은 생각을 한다.
바로 컴포넌트의 props로 들어오는 부분을 어떻게 구현할 것인가이다.
단순히 위에 적힌 props들만 타입을 정한다면 어렵지는 않다.
interface ButtonProps {
variant?: "primary" | "secondary" | "disable";
size?: "md" | "lg";
children: React.ReactNode;
}
하지만 이렇게 하면 button에 원래 있어야하는 요소들에 대한 type은 빠지게 된다.
외부에서 이 Button을 사용할 때 onClick과 같은 button이 원래 가지고 있는 기능을 사용할 수 없게 된다.
그러면 어떻게 해야할까?
물론 button의 기본 기능을 가진 타입을 넣어주면 된다!
언제나 말은 쉽다.
이전에 타입스크립트를 사용한지 얼마되지 않았을때는 다음과 같이 사용했다.
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "disable";
size?: "md" | "lg";
children: React.ReactNode;
}
사용해보면 알지만 대부분의 경우 위 방식은 잘 작동한다.
그런데 정확한 방법인가? 는 생각해봐야한다.
위 ButtonHTMLAttributes
가 어떻게 정의되어 있는지 타입이 정의된 파일로 들어가면 다음처럼 되어있다.
interface ButtonHTMLAttributes<T> extends HTMLAttributes<T> {
autoFocus?: boolean | undefined;
disabled?: boolean | undefined;
form?: string | undefined;
formAction?: string | undefined;
formEncType?: string | undefined;
formMethod?: string | undefined;
formNoValidate?: boolean | undefined;
formTarget?: string | undefined;
name?: string | undefined;
type?: 'submit' | 'reset' | 'button' | undefined;
value?: string | ReadonlyArray<string> | number | undefined;
}
button 에 사용되는 메서드들이 정의되어있다.
제네릭으로 들어온 T 는? interface 안에는 찾아볼 수 없다.
여기까지보면 어떤 html 요소가 들어와도 위 타입으로 정의하면 위 기능들을 사용해도 에러가 나오지 않는다는 것이된다.
흠...
그럼 유일하게 <T>
가 사용되고 있는 HTMLAttributes로 가보자
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
// React-specific Attributes
defaultChecked?: boolean | undefined;
defaultValue?: string | number | ReadonlyArray<string> | undefined;
suppressContentEditableWarning?: boolean | undefined;
suppressHydrationWarning?: boolean | undefined;
// Standard HTML Attributes
accessKey?: string | undefined;
className?: string | undefined;
contentEditable?: Booleanish | "inherit" | undefined;
contextMenu?: string | undefined;
dir?: string | undefined;
draggable?: Booleanish | undefined;
hidden?: boolean | undefined;
id?: string | undefined;
lang?: string | undefined;
nonce?: string | undefined;
placeholder?: string | undefined;
slot?: string | undefined;
spellCheck?: Booleanish | undefined;
style?: CSSProperties | undefined;
tabIndex?: number | undefined;
title?: string | undefined;
translate?: 'yes' | 'no' | undefined;
// Unknown
radioGroup?: string | undefined; // <command>, <menuitem>
// WAI-ARIA
role?: AriaRole | undefined;
// RDFa Attributes
about?: string | undefined;
datatype?: string | undefined;
inlist?: any;
prefix?: string | undefined;
property?: string | undefined;
resource?: string | undefined;
typeof?: string | undefined;
vocab?: string | undefined;
// Non-standard Attributes
autoCapitalize?: string | undefined;
autoCorrect?: string | undefined;
autoSave?: string | undefined;
color?: string | undefined;
itemProp?: string | undefined;
itemScope?: boolean | undefined;
itemType?: string | undefined;
itemID?: string | undefined;
itemRef?: string | undefined;
results?: number | undefined;
security?: string | undefined;
unselectable?: 'on' | 'off' | undefined;
// Living Standard
/**
* Hints at the type of data that might be entered by the user while editing the element or its contents
* @see https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute
*/
inputMode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search' | undefined;
/**
* Specify that a standard HTML element should behave like a defined custom built-in element
* @see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is
*/
is?: string | undefined;
}
와우 잘못들어왓나...? 싶지만 맞다.. ㅎㅎㅎ
HTMLAttributes 는 이렇게 타입이 정의되어 있다.
그런데 아직도 T 타입은 사용되지 않고있다.
유일하게 사용되고 있는 DOMAttributes<T>
로 가보자
interface DOMAttributes<T> {
children?: ReactNode | undefined;
dangerouslySetInnerHTML?: {
__html: string;
} | undefined;
// Clipboard Events
onCopy?: ClipboardEventHandler<T> | undefined;
onCopyCapture?: ClipboardEventHandler<T> | undefined;
... 중략
// MouseEvents
onAuxClick?: MouseEventHandler<T> | undefined;
onAuxClickCapture?: MouseEventHandler<T> | undefined;
onClick?: MouseEventHandler<T> | undefined;
onClickCapture?: MouseEventHandler<T> | undefined;
onContextMenu?: MouseEventHandler<T> | undefined;
onContextMenuCapture?: MouseEventHandler<T> | undefined;
onDoubleClick?: MouseEventHandler<T> | undefined;
onDoubleClickCapture?: MouseEventHandler<T> | undefined;
... 생략
}
정말 많이 생략했다. ㅎㅎㅎ
직접 살펴보면 알겠지만 이곳에는 거의 대부분이 Dom 이벤트에 대한 타입들이 지정되어있다. 그리고 그 이벤트에 드디어 제네릭타입 T가 사용되고 있다.
위 ButtonHTMLAttributes<T>
를 사용하고 T에 button을 지정하면 button의 이벤트를 사용하기위해 타입을 확장했다고 보면된다.
Button type에서 주로 사용하는게 onClick이기는 하다만 ...
다른 요소들도 다음처럼 지정을하면 될까?
뭐 당연히... 아니다.
이벤트는 커버를 한다해도 특정요소는 커버가 되지 않을 수 있다.
그렇때 사용할 수 있는 것이
ComponentProps
이다
React.DetailedHTMLProps 라는 것도 있다.
그런데 이것과 HTMLAttributes와의 차이는
ref?: legacyref<T>;
key?: string | number;
이 두가지가 있고 없고의 차이라고 한다.
를 사용하는 것이 특정 Dom 요소의 모든 속성을 사용할 수 있는 것이라고 한다.
사용법은 위와 비슷하다.
interface ButtonProps extends React.ComponentProps<"button"> {
variant?: "primary" | "secondary" | "disable";
size?: "md" | "lg";
children: React.ReactNode;
}
위처럼 제네릭안에 사용하는 dom 요소의 태그를 string으로 넣어주면 된다.
이러면 앞서 문제가 되었던 것들이 전부 사라지게 된다.
게다가
ComponentProps는 어떤 컴포넌트든 제네릭으로 받아올 수 있다고
한다.
예를들면
const SubmitButton = ({
onSubmit
...otherProps
}: {onSubmit: () => void}) => {
return (
<button
onClick={onSubmit}
{...otherProps}
>
Submit
</button>
);
}
type SubmitButtonProps = React.ComponentProps<typeof SubmitButton>
처럼 사용할 수 있다고 한다.
보통 컴포넌트에 ref를 넘겨줄때 react에서 React.forwardRef 를 사용해야했다.
componentProps는 ref를 연결한 버전인 ComponentPropsWithRef가 있다.
사용방법은 위와 같고 이름만 ComponentPropsWithRef로 바꿔주면 된다.
만약 ref만 빼고 사용하고 싶다면 ComponentPropsWithoutRef 를 사용하면 된다.
타입스크립트를 사용하면서 아직도 모르는것이 너무 많고 잘 못 사용하고 있는 것이 너무 많은 것 같다.
typescript를 좀 더 잘 쓸 수 있도록 해야지~
이번에 괜찮은 사이트도 하나 알게 되어 남긴다. react 에서 typescript를 사용하는 팁을 많이 알려주는 사이트인거 같다.
그럼 오늘은 여기까지
참고