Skip to main content

Command Palette

Search for a command to run...

React custom hook - useInput 정성들여 깎기

Published
•6 min read•View as Markdown

개인프로젝트의 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을 처리해주는 것들의 타입을 따로 지정해준다는 걸 생각했을 때, 저것은 확장하기에 조금 적합하지 않다는 생각이 들었음. 그리고 타입스크립트가 기본적으로 제공해주는 것들을 조금 더 사용하고 싶어짐

수정이 필요해보이는 사항

  1. 상태 업데이트의 불변성 유지: **setInputValue**에서 상태를 업데이트할 때, **isObject**가 **true**일 때 객체가 복사되고 새로운 값이 추가됨. 그러나 **inputValue**는 항상 이전 상태를 참조하므로, 상태 업데이트에 대한 불변성을 유지할 수 없음. 객체를 업데이트할 때는 이전 상태를 기반으로 새로운 객체를 생성하여 새 상태로 설정해야 함. 이를 해결하기 위해 **setInputValue**를 사용할 때, 함수를 전달하여 이전 상태를 올바르게 업데이트하는 방법을 사용해야 함.

  2. 타입 가드 필요성: **isObject**는 **typeof**를 사용하여 **inputValue**의 타입이 객체인지 확인하고 있음. 그러나 TypeScript는 현재 **isObject**가 true일 때 **inputValue**의 타입을 객체로 인식하지 않을 수 있음. 따라서 좀 더 정확한 타입 가드를 사용하여 객체 타입을 보다 안정적으로 확인하는 것이 좋다고 판단함.

  3. 객체 업데이트 관련 주의사항: 객체를 업데이트할 때, spread 연산자를 사용하여 새로운 객체를 생성하는 방법은 기존의 객체를 변경하지 않고 새로운 객체를 만들어야 하는 경우에 유용하지만, 객체의 중첩된 상태를 올바르게 업데이트하고 관리하기 위해서는 깊은 복사(deep copy)나 더 복잡한 로직이 필요할 수 있음.

  4. React.ChangeEvent<HTMLInputElement> 와 React.ChangeEvent<HTMLTextAreaElement>는 호환되지 않으므로 TextArea 인 경우에 대한 처리를 해줄 수 없음

  5. 수정에 대한 로직이라면 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 일때는 어떢하지, 값이 배열일땐 어떡하지..) 개선하면 되지~

More from this blog

번들링 최적화를 통한 import cost 줄이기

최근 팀에서 개발한 디자인 시스템 라이브러리를 프로젝트에 적용하고 사용하면서 흥미로운 문제를 발견했다.(사실 전부터 알고있었지만 잠깐 미뤄뒀다.) 단순히 Button 컴포넌트 하나만 필요했는데, 번들 분석 도구를 확인해보니 라이브러리 전체가 번들에 포함되어 있었던 것이다. 단 하나의 컴포넌트를 위해 수백 KB의 코드가 추가되어버린 것.. // 예상: Button 컴포넌트만 가져오기 import { Button } from '@company/de...

Mar 11, 20256 min read45

package.json 진입점 설정과 타입 접근성의 관계 이해하기

JavaScript와 TypeScript 패키지를 개발할 때 package.json의 진입점 설정은 단순히 어떤 파일이 먼저 로드될지를 결정하는 것 이상의 의미를 갖는다. 특히 TypeScript를 사용하는 프로젝트에서는 타입 접근성에 중요한 영향을 미친다. 회사 패키지를 수정하다가, “@패키지/dist/…” 와 같은 임포트 경로가 너무 프로젝트 내부의 구조등을 대놓고 보여주고 있기도 하고, 무엇보다 보기에 거슬려서 이를 해결하기 위해 type...

Mar 5, 20254 min read15

[2월 회고] 우당탕탕 반백수 2월

업무 이직 첫출근 2/26. 실업급여 퇴사 일주일 뒤에 새 회사에 출근을 하게 됐다. 사실 2주 정도 실업급여를 받고 나서 입사를 했다면 “조기취업수당”이라는 것을 통해 돈을 더 받을수 있었는데, 몇백만원 더 받자고 집에서 더 놀기에는 엉덩이가 들썩거리기도 하고 집에서 뇌가 굳어가는 것 같아 하루라도 빨리 입사를 하고싶었다. 그리고 나는 갈 회사가 있어서 사실상 받을 필요가 없으니 실업급여가 정말 필요한 사람들이 받아가는 게 더 맞지 않...

Feb 28, 20247 min read264
[2월 회고] 우당탕탕 반백수 2월

[1월 회고] 비전공자 출신 신입이었던 내가 6개월만에 칼이직러가 될수있었던 이유 …더보기

신년 맞이(?) 회사 경영악화로 인한 인원축소가 일어나, 잘 다니던 직장을 하루아침에 잃게되었다. 그 과정에서 다소 혼란스럽지만 먹고는 살아야겠기에 열심히(?) 이직준비..를 했던것 같다. 사실 처음에는 왜 내가 정리돼야하는지 현실부정 및 자괴감에 빠져서 아무것도 하기싫어병에 걸렸었는데, 요즘 채용시장이 안좋아서 이력서도 서류탈락의 연속이었던지라 절망의 늪에서 허우적대며 더더욱 깊이 빠져가던 차에 서류합격한 한 회사에 올인했다. 지나고나서야 하...

Feb 7, 20245 min read624

1nxeo's devlog

11 posts