React custom hook - useInput 정성들여 깎기
개인프로젝트의 useInput 커스텀 훅을 보다가 좀 더 재사용 가능하게 고치고싶어졌다.
기존 코드
import { useState } from "react";
import { InputValue } from "../../../data/type/type";
export const useInput = (initialValue: InputValue) => {
const [inputValue, setInputValue] = useState<InputValue>(initialValue);
const onChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const { name, value } = event.target;
setInputValue({ ...inputValue, [name]: value });
};
const onCheckHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = event.target;
setInputValue({
...inputValue,
[name]: checked,
});
};
return {
onChangeHandler,
onCheckHandler,
inputValue,
setInputValue, };
};
거슬리는 점
onChangeHandler와 onCheckHandler가 존재함
- 체크박스 일 경우에도 onChangeHandler 하나로 처리해주고싶음
type InputValue
를 보니, 프로젝트 내에서만 사용하는 데이터의 타입이었음앞으로 만들 프로젝트에서도 사용하고싶어짐.
현재 InputValue는 객체 형태인데, 단일 값을 가질 때에도 useInput을 사용하고 싶다고 생각함
1차 수정
import { useState } from "react";
type InputType = string | number | boolean;
interface InputValueProps {
initialValue: InputType | { [key: string]: InputType };
}
export const useInput = ({ initialValue }: InputValueProps) => {
const [inputValue, setInputValue] = useState(initialValue);
const isObject = typeof inputValue === "object";
const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, checked, type } = event.target;
const newValue = type === "checkbox" ? checked : value;
setInputValue(isObject ? { ...inputValue, [name]: newValue } : newValue);
};
return {
onChangeHandler,
inputValue,
setInputValue,
};
};
InputType을 number, string, boolean으로 정해주고, initialValue의 타입을 InputType 또는 문자열을 키로 하고 InputType을 값으로 갖는 객체로 지정해줌.
inputValue가 객체일 때(isObject === true), [name]:value 로 동적으로 키값을 변경해줌으로써 각 항목마다 조건처리를 해주지 않아도 됨
type이 checkbox일 때, checked(boolean)을 값으로 갖게 처리해줌으로 체크박스 대응
const { name, value, checked } = event.target;
const newValue = isObject ? {...inputValue, [name]:checked ?? value} : checked ?? value
처음에 이렇게 만들어놓고 checked가 존재할 때만 체크박스의 값을 처리해주도록 만들어서 와 ! 이제 됐다 ! 싶었는데(개인적으로 ??
사용하는걸 좋아함.. 왠지 귀여워서….), 생각해보니 radio 에 대한 처리를 해줄 때에는 checked 값이 있지만, value를 가져와야 하므로 해당 로직으로는 원하는 결과를 얻을 수 없다고 생각함.
const { name, value, checked, type } = event.target;
const newValue = type === "checkbox" ? checked : value;
따라서 event.target에서 type까지 가져와서 checkbox일 때에만 checked 값을 넣도록 해줌.
자 이제 되지 않았나 ! 라고 생각했지만, 보통 프로젝트에서 form이든 뭐든 input을 처리해주는 것들의 타입을 따로 지정해준다는 걸 생각했을 때, 저것은 확장하기에 조금 적합하지 않다는 생각이 들었음. 그리고 타입스크립트가 기본적으로 제공해주는 것들을 조금 더 사용하고 싶어짐
수정이 필요해보이는 사항
상태 업데이트의 불변성 유지: **
setInputValue
**에서 상태를 업데이트할 때, **isObject
**가 **true
**일 때 객체가 복사되고 새로운 값이 추가됨. 그러나 **inputValue
**는 항상 이전 상태를 참조하므로, 상태 업데이트에 대한 불변성을 유지할 수 없음. 객체를 업데이트할 때는 이전 상태를 기반으로 새로운 객체를 생성하여 새 상태로 설정해야 함. 이를 해결하기 위해 **setInputValue
**를 사용할 때, 함수를 전달하여 이전 상태를 올바르게 업데이트하는 방법을 사용해야 함.타입 가드 필요성: **
isObject
**는 **typeof
**를 사용하여 **inputValue
**의 타입이 객체인지 확인하고 있음. 그러나 TypeScript는 현재 **isObject
**가 true일 때 **inputValue
**의 타입을 객체로 인식하지 않을 수 있음. 따라서 좀 더 정확한 타입 가드를 사용하여 객체 타입을 보다 안정적으로 확인하는 것이 좋다고 판단함.객체 업데이트 관련 주의사항: 객체를 업데이트할 때, spread 연산자를 사용하여 새로운 객체를 생성하는 방법은 기존의 객체를 변경하지 않고 새로운 객체를 만들어야 하는 경우에 유용하지만, 객체의 중첩된 상태를 올바르게 업데이트하고 관리하기 위해서는 깊은 복사(deep copy)나 더 복잡한 로직이 필요할 수 있음.
React.ChangeEvent<HTMLInputElement>
와React.ChangeEvent<HTMLTextAreaElement>
는 호환되지 않으므로 TextArea 인 경우에 대한 처리를 해줄 수 없음수정에 대한 로직이라면 initialValue가 들어가야하는 것이 맞지만, 글 작성에 대한 것이라면 initialValue가 없어도 그에 맞춰서 만들어주면 좋을 것 같음 => 근데이건 어려울 것 같다.
2차 수정(현재까지는 최종본)
위의 수정이 필요해 보이는 사항에서 1, 2, 4번을 개선해보았다.
import { useState } from "react";
export const useInput = <T>(initialValue: T) => {
// const isObject = typeof initialValue === "object";
const [inputValue, setInputValue] = useState<T>(initialValue);
const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
// 다중 checkebox 처리 && 배열일 때 처리
const handleCheckbox = (arr: T[], newItem: T): T[] =>
arr.includes(newItem)
? arr.filter((item) => item !== newItem)
: [...arr, newItem];
const onChangeHandler = (
event:
| React.ChangeEvent<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>
) => {
const { name, value, type } = event.target;
const currentValue = inputValue[name];
// input 요소의 경우 type이 checkbox인지 확인하여 checked 속성 가져오기
const checked =
type === "checkbox" && !value
? (event.target as HTMLInputElement).checked
: undefined;
const newValue = checked ?? value;
// type = file 일 때 처리 해야함..
setInputValue((prev) =>
isObject(inputValue)
? Array.isArray(currentValue)
? {
...prev,
[name]: handleCheckbox(currentValue, newValue as any),
}
: ({ ...prev, [name]: newValue } as T)
: (newValue as T)
);
};
return {
onChangeHandler,
inputValue,
setInputValue,
};
};
새롭게 알게된 것
Record<string, unknown>
TypeScript에서 사용되는 타입으로, 여기서 string
타입의 키와 어떤 값이든 가질 수 있는 타입.
**Record<K, T>
**는 키 **K
**와 값 **T
**의 쌍으로 구성된 객체를 나타내는데, **string
**은 키로서 문자열을 의미하며, **unknown
**은 모든 타입을 나타냄.
즉, **Record<string, unknown>
**은 문자열 키를 갖고 어떠한 타입의 값이라도 가질 수 있는 객체를 의미. 이것은 매우 유연한 객체 타입으로, 어떠한 형태의 프로퍼티도 포함할 수 있고 값의 타입이 무엇이든 될 수 있는 객체를 표현함.
is
TypeScript에서 사용되는 타입 가드(Type Guard)
타입 가드란 런타임에서 값의 타입을 체크하고 해당 값을 특정 타입으로 좁혀주는 역할. 이를 통해 TypeScript 컴파일러에게 코드 상의 타입 정보를 더 정확하게 전달하여 타입 안전성을 유지하도록 도움.
예) **value is Record<string, unknown>
**의 형태에서 is
키워드는 value
변수가 Record<string, unknown>
타입인지 여부를 체크함. 이는 불리언(Boolean) 값을 반환하는 표현식.
**value is Record<string, unknown>
**가 **true
**라면, TypeScript는 해당 블록 내에서 **value
**를 Record<string, unknown>
타입으로 간주함. 이후에 해당 블록 내에서는 **value
**를 Record<string, unknown>
타입으로 사용할 수 있음. 이렇게 함으로써 TypeScript는 해당 블록 내에서 **value
**의 타입을 좁혀서 더 정확한 타입 추론을 할 수 있게 됨.
unknown
TypeScript에서 사용되는 타입 중 하나로, 다른 모든 타입의 상위 타입.
JavaScript의 **any
**와 유사하지만, 타입 안전성을 보장하기 위해 **any
**보다 조금 더 엄격하게 동작함.
unknown
타입은 어떠한 값도 할당할 수 있지만, 해당 값의 타입 정보에 대해 아무 것도 알지 못함. 따라서 **unknown
**으로 선언된 변수는 할당된 값의 타입 정보를 보존하면서도 안전한 방식으로 조작할 수 있도록 해줌.
**unknown
**을 사용하는 변수는 다른 타입으로 할당하기 위해서는 명시적인 타입 체크나 타입 변환을 거쳐야 함.
이를 통해 개발자가 명시적으로 타입을 보장하고 안전한 코드를 작성할 수 있도록 도움.
let userInput: unknown;
let someValue: any;
userInput = 5;
someValue = userInput; // 'unknown'은 'any'에 할당할 수 있음
let stringLength: number;
// 'unknown'을 'string'으로 타입 단언(타입 변환)
if (typeof userInput === 'string') {
stringLength = userInput.length; // 여기서 TypeScript는 'userInput'이 'string'임을 인지
}
// 'any'를 사용한 경우
let anyValue: any = 10;
let stringValue: string = anyValue; // 'any'는 'string'에 바로 할당 가능
사용법
interface UserInfo {
name:string;
age:number;
address:string;
}
// 또는
type UserInfo = string
// initialValue 설정 어쩌구 저쩌구
const { onChangeHandler, inputValue, setInputValue } = useInput<UserInfo>(initialValue);
조금 더 개선하고 싶은데..
현재까지 만든 useInput은 객체안의 객체까지는 완벽하게 적용할수 없어 보인다.
deepCopy에 대한 유틸함수를 만들고싶은데, 조금더 공부해서 만들어보고 적용해야겠음. (그냥 immer 사용..?)
JSON.parse(JSON.stringify()), structuredClone()등이 있는걸 알지만 복잡한 구조의 객체일 때 뭔가 “완벽한” 깊은복사 함수를 만들어보고싶음. 그거 만들고나면 useInput의 3차 업뎃이 있을 예정…ㅎ…
2차수정하고나서 와 super sexy 하구만 하고 자아도취중이었는데 꼐속 부족한점이 보인다. . .(type=file 일때는 어떢하지, 값이 배열일땐 어떡하지..) 개선하면 되지~
Subscribe to my newsletter
Read articles from InseoYang directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by