Post

Typescript(4) - 각 Element의 Props 속성을 정해보자

📌시작하며

컴포넌트를 만들다 보면, onClick이나 onMouseOver등의 이벤트를 props로 받아와야 하는 일이 간혹 생기곤 한다. 물론 props로 내려줘서 작업해도 문제는 없지만, 이벤트가 하나 더 발생하면, 컴포넌트에 정의해둔 props의 interface안에 작성해줘야 하는 번거로움이 생긴다.

그래서 미리 정의할 때 타입을 확장해서 해당 HTML 요소에 알맞은 props가 내려오는 것을 명시해줄 수 있다.

실제로 내가 자주 사용하고 있는 방식이라 문서로 정리해두고자 한다.

✅Button Component

버튼 컴포넌트를 만들어보자. 나는 평소 className과 children을 내려주는 방식을 자주 사용해서 이 부분도 함께 포함해서 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react"
import cn from "@utils"

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  className?: string
}

const Button = ({ className, children, ...props }: ButtonProps) => {
  return (
    <button className={cn("my-class", className)} {...props}>
      {children}
    </button>
  )
}

export default Button

✅타입 스크립트 해석하기

ButtonProps란 이름의 interface를 작성한다. 이때 ButtonProps는 React.ButtonHTMLAttributes<HTMLButtonElement>값으로 한정한다.

그럼 각각 React.ButtonHTMLAttributesHTMLButtonElement는 무슨 타입일까?

하나씩 타고 들어가서 살펴보자.

➡️React.ButtonHTMLAttributes

맨 앞에 붙어있는 React타입은 리액트에서 사용할 JSX의 타입 정의이다.

1
2
3
4
5
6
declare module "react" {
  interface StyleHTMLAttributes<T> extends HTMLAttributes<T> {
    jsx?: boolean
    global?: boolean
  }
}

React에서 사용할 JSX 타입 중에서도 ButtonHTMLAttribues 타입을 사용하겠다는 의미가 된다. 이때 ButtonHTMLAttribues은 이름 그대로, button태그에서 사용하는 속성들을 포함한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ButtonHTMLAttributes<T> extends HTMLAttributes<T> {
  disabled?: boolean | undefined
  form?: string | undefined
  formAction?:
    | string
    | DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS]
    | undefined
  formEncType?: string | undefined
  formMethod?: string | undefined
  formNoValidate?: boolean | undefined
  formTarget?: string | undefined
  name?: string | undefined
  type?: "submit" | "reset" | "button" | undefined
  value?: string | readonly string[] | number | undefined
}

➡️HTMLButtonElement

HTMLButtonElement는 button 요소의 동작을 제어하고 조작하는 속성과 메서드를 포함한다. 즉, React.ButtonHTMLAttributes가 리액트에서 사용할 button의 jsx props 를 정의한다면, HTMLButtonElement는 실제 button 태그의 DOM 요소를 조작하는 속성과 메서드를 정의하고 있는 것이다.

이 두가지를 같이 사용해 button 태그에 관련된 props와 DOM 조작을 정해진 타입 안에서 안전하게 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
interface HTMLButtonElement extends HTMLElement, PopoverInvokerElement {
  disabled: boolean
  readonly form: HTMLFormElement | null
  formAction: string
  formEnctype: string
  formMethod: string
  formNoValidate: boolean
  formTarget: string
  readonly labels: NodeListOf<HTMLLabelElement>
  name: string
  type: "submit" | "reset" | "button"
  readonly validationMessage: string
  readonly validity: ValidityState
  value: string
  readonly willValidate: boolean
  checkValidity(): boolean
  reportValidity(): boolean
  setCustomValidity(error: string): void
  addEventListener<K extends keyof HTMLElementEventMap>(
    type: K,
    listener: (this: HTMLButtonElement, ev: HTMLElementEventMap[K]) => any,
    options?: boolean | AddEventListenerOptions
  ): void
  addEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions
  ): void
  removeEventListener<K extends keyof HTMLElementEventMap>(
    type: K,
    listener: (this: HTMLButtonElement, ev: HTMLElementEventMap[K]) => any,
    options?: boolean | EventListenerOptions
  ): void
  removeEventListener(
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | EventListenerOptions
  ): void
}

✅응용하기

이 기능을 이용해 다른 html 태그도 얼마든지 만들어 낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react"
import cn from "@utils"

interface DivProps extends React.DivHTMLAttributes<HTMLDivElement> {
  className?: string
}

const Div = ({ className, children, ...props }: DivProps) => {
  return (
    <div className={cn("my-class", className)} {...props}>
      {children}
    </div>
  )
}

export default Div

이 방식을 사용하면 내가 만든 컴포넌트들에 onClick 같은 메소드를 props로 받겠다고 하나하나 작성하지 않아도 된다.

➡️ 기존 방식을 사용할 때

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from "react"
import cn from "@utils"

const Parent = () => {
  const [setOpen, setIsOpen] = useState<boolean>(false)

  return (
    <div>
      <Div setIsOpen={setIsOpen}>클릭하면 닫힌다.</Div>
    </div>
  )
}

interface DivProps {
  className?: string
  setOpen: () => boolean
}

const Div = ({ className, children, setOpen }: DivProps) => {
  return (
    <div
      className={cn("my-class", className)}
      onClick={() => {
        setOpen(false)
      }}
    >
      {children}
    </div>
  )
}

export default Div

➡️ 타입을 한정할 떄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useState } from "react"
import cn from "@utils"

const Parent = () => {
  const [setIsOpen, setOpen] = useState<boolean>(false)

  return (
    <div>
      <Div onClick={() => setOpen(false)}>클릭하면 닫힌다.</Div>
    </div>
  )
}

interface DivProps extends React.HTMLProps<HTMLDivElement> {
  className?: string
}

const Div = ({ className, children, ...props }: DivProps) => {
  return (
    <div className={cn("my-class", className)} {...props}>
      {children}
    </div>
  )
}

export default Div

🗂️참고 사이트

This post is licensed under CC BY 4.0 by the author.