회원 가입과 로그인 폼에서 공통으로 사용하고 있는 Input 컴포넌트의 props 타입을 지정하는 과정에서 제네릭을 활용하는 방법을 공유하고자 합니다. React-hook-form을 사용하면서 저처럼 타입 지정에 어려움을 겪으신 분들에게 도움이 되면 좋겠습니다.
우선, 제네릭에 대해 간단하게 알아보고, 코드를 보면서 작성해 나가겠습니다.
제네릭 타입
개념
제네릭은 말 그대로 일반화된 데이터 타입으로, 함수, 타입, 클래스 등에서 사용할 타입을 미리 정해두지 않고, 타입 변수를 사용해 해당 위치를 비워둔 다음, 실제로 그 값을 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식입니다. 들어올 수 있는 여러 타입을 일일이 지정해주지 않아도 되기 때문에 재사용성이 크게 향상된다는 장점이 있습니다.
타입 변수는 일반적으로 <T>와 같이 정의하며, 사용할 때는 매개변수를 넣는 것과 유사하게 원하는 타입을 넣어줍니다. 보통 타입 변수명으로 T(Type), E(Element), K(Key), V(Value) 등 한 글자로 된 이름을 많이 사용합니다.
type GenericArrayType<T> = T[]
const array1: GenericArrayType<string> = ["자바스크립트", "타입스크립트"]
const array2: GenericArrayType<number> = [1, 2, 3]
const array3: GenericArrayType<boolean> = [true, false, true]

위처럼 <> 안에 지정한 타입이 전달 되어 string[]로서 동작함을 알 수 있습니다.
주의
제네릭을 일반화된 데이터 타입을 의미하므로 함수나 클래스 내부에서 제네릭을 사용할 때 어떤 타입이든 될 수 있다는 개념을 알고 있어야 합니다. 특정한 타입에서만 존재하는 멤버를 참조하려고 하면 안 됩니다.
function getTextLength<T>(text: T): T {
return text.length; // Property 'length' does not exist on type 'T'.
}
다시 말해, 여기에서 T는 어떤 타입도 들어올 수 있기 때문에 함부로 length를 참조하면 안 됩니다.
그렇다면 T가 length를 가지는 타입만 들어오도록 하면 어떨까요?
interface TypeWithLength {
length: number
}
function logText<T extends TypeWithLength>(text: T): T {
console.log(text.length);
return text;
}
이렇게 제네릭에서 extends를 사용하면 length 속성을 가진 타입만 받는다는 제약을 걸어주어, length 속성을 사용할 수 있게 해 줍니다.
React-hook-form과 사용하기
다른 스키마를 기반으로 하는 폼의 메서드를 자식 컴포넌트로 넘겨주는 경우, 이에 대한 제네릭을 사용하여 해당 위치를 비워두고 실제로 그 값을 사용할 때 지정되도록 할 수 있습니다. 먼저, 살펴 볼 컴포넌트는 크게 1. 로그인 폼 2. 회원가입 폼 3. 공통 Input입니다.
로그인 폼
// LoginForm.tsx
const LoginForm = () => {
// ...
const { handleSubmit, control, trigger } = useForm<LoginSchema>({
resolver: zodResolver(loginSchema),
});
return (
<Form onSubmit={handleSubmit(onSubmitLogin)} name={formType}>
<Title>NutriNotes</Title>
<FormTitle>{formType.toUpperCase()}</FormTitle>
<Input name="email" type="text" control={control} trigger={trigger} formType={formType} />
<Input
name="password"
type="password"
control={control}
trigger={trigger}
formType={formType}
/>
<BottomContainer>
<Message to="/register">Create an account</Message>
<ConfirmButton type="submit">Next</ConfirmButton>
</BottomContainer>
</Form>
);
};
로그인 폼은 LoginSchema를 기반으로 하는 폼이고, 이메일과 비밀번호를 입력하는 Input을 가지고 있습니다.
회원가입 폼은 RegisterSchema를 기반으로 이메일, 비밀번호 Input 이외에 추가적으로 비밀번호 확인, 유저이름 Input을 가지고 있습니다.
type LoginSchema = {
email: string;
password: string;
}
type RegisterSchema = {
email: string;
password: string;
passwordConfirm: string;
username: string;
}
이를 기반으로 우선 Input을 사용할 때 받아올 props의 타입을 지정해 보겠습니다. 먼저 Input의 props은 크게 2가지로 나눌 수 있습니다. 1. useController의 props로 전달할 값: name, controller
2. 이외 Input 컴포넌트에서 사용할 값: name, type, trigger, formType,...
useController의 props 타입은 react-hook-form에서 제공되는 UseControllerProps<T extends FieldVluaes>입니다.
그 이외의 props는 아래처럼 사용자 지정 타입을 만들어줍니다.
type InputProps<T extends FieldValues> = {
type: string;
trigger: UseFormTrigger<T>;
formType?: string;
disabled?: boolean;
onUpdate?: (isDuplicated: boolean) => void;
};
그리고 이 둘을 모두 만족하는 FormInputProps 타입을 만듭니다.
type FormInputProps<T extends FieldValues> = InputProps<T> & UseControllerProps<T>;
이때 제네릭 T는 항상 FieldValues에 할당할 수 있는 값으로 한정합니다.
마지막으로, Input 컴포넌트에 이렇게 만든 제네릭을 지정합니다.
const Input = <T extends FieldValues>({
name,
type,
control,
trigger,
onUpdate,
formType,
disabled = false,
}: FormInputProps<T>) => {
// ...
}
완성된 모습은 아래와 같습니다.
import { FieldValues } from 'react-hook-form';
type InputProps<T extends FieldValues> = {
type: string;
formType?: string;
disabled?: boolean;
trigger: UseFormTrigger<T>;
onUpdate?: (isDuplicated: boolean) => void;
};
type FormInputProps<T extends FieldValues> = InputProps<T> & UseControllerProps<T>;
const Input = <T extends FieldValues>({
name,
type,
control,
trigger,
onUpdate,
formType,
disabled = false,
}: FormInputProps<T>) => {
const {
field: { value, onChange },
fieldState: { error, invalid, isDirty },
} = useController<T>({
name,
control,
});
// ...
}
생각
이번 포스팅에서는 React-hook-form과 제네릭을 활용해 다양한 폼 스키마에 대응하는 Input props 타입을 유연하게 지정하는 방법을 알아보았습니다. 제네릭을 이용함으로써, 하나의 Input 컴포넌트를 여러 상황에서 재사용할 수 있었고, 이는 타입 안정성을 보장하면서 코드의 중복을 줄이고 유지보수성을 높이는 역할을 합니다. 현재 우아한 타입스크립트를 읽고 있는데, 단순히 기술 서적을 읽는 것을 넘어 적용해 볼 수 있는 의미 있는 시간이었습니다.
REF
우아한 타입스크립트 with React
'FrontEnd > TypeScript' 카테고리의 다른 글
[TypeScript] 상수 선언에 왜 enum 대신 as const를 사용했나요? (0) | 2023.11.14 |
---|---|
[TypeScript] 비동기 프로그래밍, 동시성과 병렬성 (0) | 2023.06.07 |
[TypeScript] 에러 처리 (0) | 2023.06.05 |
[TypeScript] 가변성(슈퍼타입/서브타입 파악하기) & 할당성 & 타입 넓히기 (0) | 2023.06.03 |
[TypeScript] 함수의 타입 (0) | 2023.06.03 |