2023.08.16 - [source-code/React] - React Hook Form 기반 유효성 버튼 구현
여전히 react-hook-form의 도움을 받고 있다.
현 서비스의 PC 버전은 nextJS + Typescript를 사용하고 있다.
앱에 있는 기능이 모두 구현돼야 하므로... 역시나 수많은 form 관련 로직을 다룬다.
→ react-hook-form도 TS로 작성하는 중!
// Input.tsx
import {DetailedHTMLProps, InputHTMLAttributes, ReactNode} from 'react'
type InputProps = DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>
const Input = (props: InputProps) => {
return <input {...props} />
}
atom단위가 되는 input 컴포넌트!
해당 컴포넌트의 props로 react-hook-form과 관련된 상태를 넘겨주기보다는,
해당 Input을 합성한, react hook form용 FormInput을 만들어주면 좋을 테다.
// FormInout.tsx
import React, {useState} from 'react'
import {FieldError, useController, UseControllerProps} from 'react-hook-form'
import Input, {InputProps} from './Input'
const FormInput = (props: InputProps & UseControllerProps<any>) => {
const {name, control, rules, ...restInputProps} = props
const {
field: {onChange, value},
formState: {errors},
} = useController({
name,
control,
rules,
})
return (
<Input
onChange={onChange}
value={value}
{...restInputProps}
/>
)
}
useController hooks를 통해 재사용이 가능한 react-hook-form용 input을 작성했다.
(onChange, value, erros 등을 custom 해 작성해주기 위해 비구조할당을 한 모습)
// Page.tsx
cosnt Page = () => {
const {control} = useForm({
defaultValues : {
name : '이름',
phone_number : '010-0000-0000"
}
})
...
return(
<form>
<FormInput
name="name"
control={control}
placeholder="이름을 입력해주세요"
rules={{
required : '이름을 반드시 입력해주세요"
}}
/>
<FormInput
name="phone_number"
control={control}
placeholder="전화번호를 입력해주세요"
rules={{
required : '전화번호를 반드시 입력해주세요"
}}
/>
</form>
)
}
form이 필요한 page에서 다음과 같이 쓸 수 있다.
control을 props로 넘겨줌으로써, react-hook-form의 여러 input 제어 기능을 사용 가능.
(submit, error처리 등)
그런데 여기서 문제는...
// FormInput.tsx
const FormInput = (props: InputProps & UseControllerProps<any>) => {
바로 이 부분...!
우리의 FormInput의 props는
html Input의 props type과(여기서는 InputProps)
useController의 props type을 합친 union type이 될 테다.
export type UseControllerProps<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = {
name: TName;
rules?: Omit<RegisterOptions<TFieldValues, TName>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>;
shouldUnregister?: boolean;
defaultValue?: FieldPathValue<TFieldValues, TName>;
control?: Control<TFieldValues>;
};
UseControllerProps는 다음과 같은 type.
그런데 다음과 같이 작성할 경우...
FormInput의 props 중 name 속성에, 우리가 의도하지 않은 값이 들어가도, 타입 체크가 불가능하다!
왜? UseControllerProps<any>로 작성했으니..!
우리가 원하는 건
FieldPath<TFieldValues>를 상속받은 TName 제네릭의 name과,
Control<TFieldValues> 타입의 control이 서로를 참조하게 만드는 것이다!
지금은 TFieldValues에 any를 넘겨줬기 때문에, name과 control의 타입 추론은 포기된 상황이다..!
// Page.tsx
cosnt Page = () => {
const {control} = useForm({
defaultValues : {
name : '이름',
phone_number : '010-0000-0000"
}
})
...
return(
<form>
<FormInput
name="nameeeeee"
control={control}
placeholder="이름을 입력해주세요"
rules={{
required : '이름을 반드시 입력해주세요"
}}
/>
<FormInput
name="phone_numberrrrrrr"
control={control}
placeholder="전화번호를 입력해주세요"
rules={{
required : '전화번호를 반드시 입력해주세요"
}}
/>
</form>
)
}
그래서 다음과 같은 끔찍한 일도 가능해진다!!!!!!
control 객체가 defaultValues옵션으로 인해 Control<{name:string, phone_number:string}>으로 타입 추론이 가능함에도 불구!
name에 nameeee, phone_numberrr이라는 엉뚱한 값이 들어가도, 이에 대한 타입 가드가 불가능 해진다.
const FormInput = (
props: InputProps &
UseControllerProps<{
name: string
phone_number: string
}>,
) ...
해결책은? 간단하다!
UseControllerProps의 제네릭으로 namer과 phone_number를 넘겨주면 그만일 테다.
TFieldValues 제네릭으로 넘겨준 타입만 허용하기 때문에, name과 phone_number 이외의 값은 타입 가드가 가능하다.
하지만 이러면 더 큰 문제가 발생한다.
→ FormInput 컴포넌트를 아무 곳에서도 재사용할 수가 없으니!
그렇다면 어떻게 해야 할까?
우리가 원하는 FormInput 컴포넌트는, 각 form에 따라 허용하는 type들이 달라지는 것!이고...
이는 → TS의 generics를 통해 구현할 수 있다!
그런데 이때... generics을 어떻게 사용할 수 있는 거지?
넘겨줄 generics가 정해지는 곳은, 각 FormInput 컴포넌트가 사용되는 곳이며...
따라서 해당 generics은 → 사용처에서 FormInput 컴포넌트에 직접 넘겨주면 될 테다!!!
const FormInput = <FormType extends {[key: string]: any}>(
props: InputProps & UseControllerProps<FormType>,
) => {
컴포넌트를 제네릭 함수처럼 써준 모습...!
그렇다면 사용처에서는?
// Page.tsx
const DEFAULT_DATA = {
name: '',
phone_number: '',
}
cosnt Page = () => {
const {control} = useForm({
defaultValues : DEFAULT_DATA
})
...
return(
<form>
<FormInput<typeof DEFAULT_DATA>
name="nameeeeee"
control={control}
placeholder="이름을 입력해주세요"
rules={{
required : '이름을 반드시 입력해주세요"
}}
/>
<FormInput<typeof DEFAULT_DATA>
name="phone_numberrrrrrr"
control={control}
placeholder="전화번호를 입력해주세요"
rules={{
required : '전화번호를 반드시 입력해주세요"
}}
/>
</form>
)
}
typeof DEFAULT_DATA를 제네릭으로 넘겨줌으로써,
useform의 control과 name 속성의 type을 일치시켜 줄 수 있다!
아까처럼 nameeee을 넘겨줄 경우, 훌륭한 타입 가드가 이뤄진다! 와!
TS를 처음 사용할 때는 꽤나 겁을 먹었었다.
라이브러리를 뜯어보면 나오는 수많은! 유니온 타입과 제네릭을 보며 마냥 어렵다는 생각부터 가졌다.
하지만 이렇게...
직접 라이브러리의 타입을 뜯어보고, 내게 맞는 타입으로 여러 번 적용하다 보니
어렵다! 보다는 굉장히 재미있고 유용한 언어라는 생각이 가득.
(이번 연도 1월부터 본격적으로 사용했는데, 뭔가 벌써 익숙해진 것 같아 약간의 뿌듯함도 든다. 하하!)
'source-code > TypeScript' 카테고리의 다른 글
"'React'은(는) UMD 전역을 참조하지만 현재 파일은 모듈입니다" 에러 해결 (2) | 2023.11.18 |
---|---|
getElementsByClassName에 for each 사용해 HTMLElement 접근하기 (0) | 2023.11.16 |
index signature 관련 타입 에러 (0) | 2023.08.21 |
JsToTs : axios (0) | 2023.08.21 |
JsToTs : 비동기 처리 - Promise (0) | 2023.08.21 |