사이드 프로젝트를 하던 와중, 프로젝트에 빈번하게 사용되는 Form을 편하게 사용하기 위해 UseForm
Custom Hook을 개발하고 있었다.
initialValue를 인자로 받아 Form과 양방향 바인딩 될 formData
, 유효성 검사를 위한 isError
, 그리고 다양한 Type의 Input들의 onChange를 핸들링 하기 위한 함수가 포함되어 있다.
유효성 검사를 위해 필요한 isError
는, 예를 들어 initialValue
로 { firstName: '', lastName: ''}
라는 객체가 들어왔을 때, 해당 객체와 같은 key를 가지고, value는 boolean Type인 객체를 { firstName: false, lastName: false }
와 같이 생성할 필요가 있었다.
그래서 처음엔 다음과 같이 Custom Hook을 작성하였다.
const makeSameKeyObj = (obj: any) => {
const newObj = { ...obj };
for (const key in newObj) {
newObj[key] = false;
}
return newObj;
};
export const useForm = <T>(initialValue: T) => {
const [formData, setFormData] = useState<T>(initialValue);
const [isError, setIsError] = useState(makeSameKeyObj(initialValue));
...
}
이렇게 작성하니 작동은 잘 하였다!
다만 문제점은, makeSameKeyObj
함수의 인자 type이 any로 되어있어 자동완성이 안되고, 컴파일러가 에러를 잡지 못한다는 점 등 Typescript의 장점을 하나도 살리지 못하는 코드였다.
makeSameKeyObj
함수를 Generic을 활용하여 작성하였다.
우선 인자로 같은 Key를 가진 객체, 그리고 초기값인 defaultValue
를 받는다.
const makeSameKeyObj = <T extends object, U>(obj: T, defaultValue: U) =>
이후 Reduce 함수를 통해 같은 Key를 가지고, 초기값은 defaultValue
인 새로운 객체를 반환하도록 하였다. 초기값은 Record를 통해 타입 지정을 해주었다.
Object.keys(obj).reduce(
(newObj, key) => ({
...newObj,
[key]: defaultValue,
}),
{} as Record<keyof T, U>
);
완성된 코드는 다음과 같다. 에디터를 통해 자동완성도 잘 되고, 에러도 잘 잡는 것을 확인하였다.
const makeSameKeyObj = <T extends object, U>(obj: T, defaultValue: U) =>
Object.keys(obj).reduce(
(newObj, key) => ({
...newObj,
[key]: defaultValue,
}),
{} as Record<keyof T, U>
);
export const useForm = <T extends object>(initialValue: T) => {
const [formData, setFormData] = useState<T>(initialValue);
const [isError, setIsError] = useState<Record<keyof T, boolean>>(
makeSameKeyObj(initialValue, false)
);