2-1 과제 리팩토링 고민하기 - 커스텀 훅에 대한 고민

GY·2022년 2월 14일
0

원티드 프리온보딩

목록 보기
5/12
post-thumbnail

커스텀 훅...아직 익숙하지 않다.

요청 대시보드 만들기 과제에서 사이드바와 토글 버튼의 경우 영역 밖을 누르면 드롭다운과 사이드바가 다시 사라지도록 만들어야 했다.
사이드바는 배경을 주어 해당 배경을 누르면 들어가도록 만들면 됐는데, 필터버튼의 경우 드롭다운에는 별도의 배경이 없었다.
두 군데에서 사용되었기 때문에 깔끔하게 커스텀 훅이나 utils함수로 빼서 공통으로 사용하고 싶었으나, 이 차이점 때문에 고민하다가 결국 따로 만들어 사용했다.

같은 과제를 수행했던 다른 팀 중 커스텀 훅을 정말 잘 사용한 팀이 있어, 어떻게 사용했는지 코드를 하나하나 뜯어보고 추후 리팩토링할 때 참고해보기로 했다.

마침 커스텀 훅으로 많이 사용되는 다른 기능들도 구현되어 있어 함께 살펴보았다.

커스텀 훅을 만들어 사용하는 것이 아직 익숙하지 않다. 하지만 좋은 코드를 작성하기 위해 꼭 필요한 부분이라고 생각했기 때문에, 프로젝트가 끝난 시점에서 다시한번 고민해보았다.

useClickAway


import { RefObject, useEffect, useRef } from 'react';
import { off, on } from '@utils/functions';

const defaultEvents = ['mousedown', 'touchstart'];

const useClickAway = <E extends Event = Event>(
  ref: RefObject<HTMLElement | null>,
  onClickAway: (event: E) => void,
  events: string[] = defaultEvents
) => {
  const savedCallback = useRef(onClickAway);
  useEffect(() => {
    const handler = (event: any) => {
      const { current: el } = ref;
      el && !el.contains(event.target) && savedCallback.current(event);
    };

    for (const eventName of events) {
      on(document, eventName, handler);
    }
    return () => {
      for (const eventName of events) {
        off(document, eventName, handler);
      }
    };
  }, [events, ref]);
};

export default useClickAway;

on, off

인자로 전달받은 요소에 원하는 event이름과 eventhandler가 될 함수를 추가하거나 제거하는 함수이다.
쉽게말해 El.addEventListener("click", onClick)와 같이 이벤트를 등록하는 것을,
on(document, "click", onClick) 로 인자를 전달받으면
on 에서 document.addEventListener("click", onClick)으로 만들어주는 것이다.

export function on<T extends Window | Document | HTMLElement | EventTarget>(
  obj: T | null,
  ...args: Parameters<T['addEventListener']> | [string | null, ...any]
): void {
  if (obj && obj.addEventListener) {
    obj.addEventListener(
      ...(args as Parameters<HTMLElement['addEventListener']>)
    );
  }
}

export function off<T extends Window | Document | HTMLElement | EventTarget>(
  obj: T | null,
  ...args: Parameters<T['removeEventListener']> | [string | null, ...any]
): void {
  if (obj && obj.removeEventListener) {
    obj.removeEventListener(
      ...(args as Parameters<HTMLElement['removeEventListener']>)
    );
  }
}

사용

const DropdownsRef = useRef(null);
const { isToggle, setState, onToggle } = useToggle({ initialState: false });
useClickAway(DropdownsRef, () => setState(false));

  return (
    <S.Form onClick={onToggle} checkedList={checkedList} ref={DropdownsRef}>
      <span>
        {filterTypeToKorean}
        {checkedList > 0 && `(${checkedList})`}
      </span>
      <ArrowDropdown />
      {isToggle && (
        <Dropbox
          filterType={filterType}
          dataList={dataList}
          setMethodList={setMethodList}
          setMaterialList={setMaterialList}
        />
      )}
    </S.Form>
  );

useToggle은 아래의 내용을 참고하자.
1. Dropbox 컴포넌트는 isToggle이 참일 때만 보인다.
2. 이 isToggle은 setState로 업데이트한다.
3. useClickAway

useToggle


import { useCallback, useState } from 'react';

const useToggle = ({ initialState = false }) => {
  const [isToggle, setState] = useState(initialState);
  const onToggle = useCallback(() => setState(() => !isToggle), [isToggle]);

  return { isToggle, onToggle, setState };
};

export default useToggle;

이렇게 사용했다.

const Toggle = forwardRef(
  (
    {
      isToggle,
      disabled,
      onChange,
      name,
      children,
      ...props
    }: Partial<ForwardProps>,
    ref: ForwardedRef<HTMLInputElement>
  ) => {
    const { isToggle: checked, onToggle } = useToggle({
      initialState: isToggle,
    });
    const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
      onToggle();
      onChange && onChange(e);
    };

    return (
      <S.ToggleContainer {...props}>
        <S.ToggleInput
          type="checkbox"
          checked={checked}
          disabled={disabled}
          onChange={handleChange}
          ref={ref}
          name={name}
        />
    );

useAxios

내부에 state안에 axios로 fetch해온 데이터를 넣은 뒤 리턴해주었다.


import { useEffect, useState } from 'react';
import axios from 'axios';

const useAxios = <T>(URL: string) => {
  const [state, setState] = useState<T>();

  useEffect(() => {
    const request = async () => {
      const { data } = await axios(URL);
      setState(data);
    };

    request();
  }, []);

  return state;
};

export default useAxios;

사용은 이렇게 했다.

interface ICardData {
  id: number;
  title: string;
  client: string;
  due: string;
  count: number;
  amount: number;
  method: string[];
  material: string[];
  status: statusType;
}
  const data = useAxios<ICardData[]>(
    'url'
  );

Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글