본문 바로가기

FrontEnd/react

Typescript, MUI (Material UI v5), React hook form 함께 사용하기 (with Generic) (2/2)

이전 포스트 에서 만들었던 FormInput 컴포넌트의 두가지 문제점을 를 제네릭을 이용해서 개선해보도록 하겠습니다. 

 

[개선된 예제]

  • types/props.d.ts
import { ControllerRenderProps, FieldValues, UseControllerProps } from 'react-hook-form'

type WithoutFormPropsComponent<ComponentProps = unknown> = Omit<ComponentProps, keyof ControllerRenderProps>

export type FormComponentProps<
  Field extends FieldValues,
  ComponentProps = unknown,
> = WithoutFormPropsComponent<ComponentProps> & UseControllerProps<Field>

여러가지 컴포넌트를 form 컴포넌트로 만드는 경우 중복되는 타입 선언을 없애기 위해 FormComponentProps 타입을 만들어줍니다. 

  - WithoutFormPropsComponent : 기존 컴포넌트의 Props에서 ControllerRenderProps 와 겹치는 props key를 제외시킨 타입입니다. 
  - FormComponentProps : 제네릭의 첫번째 인자(Field)는 FieldValues의 확장타입,  두번째 인자(ComponentProps)는 사용할 컴포넌트 props의 타입을 주입받아 UseControllerProps<Field> 와 WithoutFormPropsComponent<ComponentProps>를 인터섹션을 한 타입입니다. 

 

복잡해 보일수 있지만 ComponentProps에 TextFieldProps를 주입하면 이전 포스트 의 FormInputProps 타입에서 Field 제네릭을 추가한것과 같은 의미입니다.  

  • FormInput/FormInput.d.ts
import { TextFieldProps } from '@mui/material'
import { FormComponentProps } from 'types/props'
import { FieldValues } from 'react-hook-form'

export type Validator = (v: string) => boolean

export interface FormInputProps<T extends FieldValues> extends FormComponentProps<T, TextFieldProps> {
  validator?: Validator
}

export type OnChangeWithValidator = (validator?: Validator) => TextFieldProps['onChange']

  - Validator : react-hook-form 에서 유효성 검사 기능을 제공하지만 추가적인 유효성 검사로직이 필요한 경우 사용하기 위한 validator 함수 타입입니다.
  - FormInputProps : 위에서 선언한 FormComponentProps의 두번째 제네릭 인자(ComponentProps)에 TextFieldProps 를 추가하고, Field 에 대한 제네릭은 외부에서 주입받는 타입입니다.
  - OnChangeWithValidator : FormInput의 onChange 이벤트 핸들러 타입, validator를 인자로 받아 원래 TextFieldProps의 onChange 핸들러를 반환합니다. 

 

  • FormInput/index.tsx
import { TextField } from '@mui/material'
import { FieldValues, useController } from 'react-hook-form'
import { FormInputProps, OnChangeWithValidator } from './FormInput'

export default function FormInput<T extends FieldValues>({
  name,
  rules,
  control,
  shouldUnregister,
  defaultValue,
  validator,
  ...textFieldProps
}: FormInputProps<T>) {
  const { field } = useController({
    name,
    rules,
    control,
    shouldUnregister,
    defaultValue,
  })
  const { onChange, ...restField } = field
  const handleChange: OnChangeWithValidator = validator => e => {
    if (!validator || validator(e.target.value)) {
      onChange(e)
    }
  }

  return <TextField onChange={handleChange(validator)} {...restField} {...textFieldProps} />
}

이전 포스트 의 FormInput 컴포넌트에서 Field 에 대한 타입을 이 컴포넌트를 사용하는 컴포넌트에서 주입하도록 개선하고, onChange 이벤트에서 유효성검사로직을 추가한 컴포넌트입니다. 

 

자 이제 이 컴포넌트를 사용해보겠습니다. 

  • App.tsx
import { Box, Button } from '@mui/material'
import FormInput from 'components/FormInput'
import React from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { container, layout } from 'styles'
import { timeValidator } from 'utils'

const defaultValues = {
  FormInput: 12,
}
type FieldType = typeof defaultValues

export default function App() {
  const { control, handleSubmit } = useForm({
    reValidateMode: 'onBlur',
    defaultValues,
  })

  const handleOnSubmit: SubmitHandler<FieldType> = data => {
    alert(data.FormInput)
  }

  return (
    <Box sx={layout} component="form" onSubmit={handleSubmit(handleOnSubmit)}>
      <Box sx={container}>
        <FormInput control={control} name="FormInput" label={'Input'} />
        <Button type="submit">Submit</Button>
      </Box>
    </Box>
  )
}

사용법은 이전 포스트 와 다른점이 거의 없습니다.

useForm 의 제네릭인자를 any로 지정하지 않아도 control에서 타입에러를 발생시키지 않습니다. 

name 프로퍼티에 전체 string 타입이 아닌 정확히 FieldType의 key 만 지정할 수 있습니다. 

또 onSumbit 핸들러의 data 인자에서 FieldType에 지정한 키로 접근할 수 있습니다. 

 

FormInput 컴포넌트가 제네릭을 가진 컴포넌트이고, control 프로퍼티로 Field에 대한 제네릭타입을 주입받기 때문입니다. 이렇게해서 재사용성이 높고 any 타입을 사용하지 않으면서 타입체커로 좀 더 개발자 친화적으로 사용할 수 있는 MUI 기반의 form 컴포넌트를 만들어봤습니다.

 

전체 코드는 repository 에서 다운 받을 수 있으며, repository에는 MUI 의 Autocomplete 컴포넌트를 form 컴포넌트로 활용할 수 있는 예제가 포함되어 있습니다.  

 

참고

https://ui.toast.com/weekly-pick/ko_20210505

 

리액트 컴포넌트를 타입스크립트 제네릭 함수처럼 쓰기

리액트 컴포넌트를 사용할 때 props 타입을 제네릭 패턴으로 정의하고, 정의한 타입들로 타입스크립트의 도움을 받아보자!

ui.toast.com