프론트엔드 데브코스 5기 TIL 53 - 커스텀훅 연습하기

김영현·2023년 12월 11일
1

TIL

목록 보기
62/129

useHover

hover을 체크한다.

const useHover = () => {
  const [state, setState] = useState(false);
  const ref = useRef(null);

  const handleMouseOver = useCallback(() => setState(true), []);
  const handleMouseOut = useCallback(() => setState(false), []);

  useEffect(() => {
    const element = ref.current;
    if (!element) {
      return;
    }

    element.addEventListener("mouseover", handleMouseOver);
    element.addEventListener("mouseout", handleMouseOut);

    return () => {
      element.removeEventListener("mouseover", handleMouseOver);
      element.removeEventListener("mouseout", handleMouseOut);
    };
  }, [ref, handleMouseOut, handleMouseOver]);
  return [ref, state];
};

useCallback이 어떤 커스텀훅에서는 쓰이고 어떤 커스텀 훅에서는 쓰이지 않는다(대부분 쓰긴함). 왜 쓰이는걸까?

=> handleMouseOver처럼 이벤트 핸들러는 자주 호출된다. 매 렌더링시 재생성되는 비효율을 막기위함인가?


useScroll(reuqestAnimationFrame)

스크롤시 일어나는 이벤트를 감지한다.
이 커스텀 훅에서 무려 requestAnimationFrame을 사용한다..!!
이벤트 루프시간에 배웠던 새로운 대기열!

//useScroll.js
const useScroll = () => {
  const [state, setState] = useRafState({ x: 0, y: 0 });
  const ref = useRef(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) {
      return;
    }

    const handleScroll = () => {
      setState({
        x: ref.current.scrollLeft,
        y: ref.current.scrollTop,
      });
    };

    element.addEventListener("scroll", handleScroll, { passive: true });
    return () => {
      element.removeEventListener("scroll", handleScroll);
    };
  }, [ref]);

  return [ref, state];
};

//useRafState.js
const useRafState = (initialState) => {
  const frame = useRef(0);
  const [state, setState] = useState(initialState);

  const setRafState = useCallback((value) => {
    cancelAnimationFrame(frame.current);

    frame.current = requestAnimationFrame(() => {
      setState(value);
    });
  }, []);

  return [state, setRafState];
};

여기서 왜 useRef에 숫자를 할당하여 사용할까?
=> debounce를 구현할때도 setTimeout에 변수를 할당해준 것 처럼, requestAnimationFrame특정한 Id를 할당한다. 이 Id를 가지고 애니메이션을 지운다. setInerval의 clearTimeout기능이라고 보면됨.


useKey, useKeyPress

//useKey.js
const useKey = (event = "keydown", targetKey, handler) => {
  const handleKey = useCallback(
    ({ key }) => {
      if (key === targetKey) {
        handler();
      }
    },
    [targetKey, handler]
  );
  useEffect(() => {
    window.addEventListener(event, handleKey);
    return () => {
      window.removeEventListener(event, handleKey);
    };
  }, [event, targetKey, handleKey]);
};

//useKeyPress
const useKeyPress = (targetKey) => {
  const [keyPressed, setKeyPressed] = useState(false);

  const handleKeyDown = useCallback(
    ({ key }) => {
      if (key === targetKey) {
        setKeyPressed(true);
      }
    },
    [targetKey]
  );
  const handleKeyUp = useCallback(
    ({ key }) => {
      if (key === targetKey) {
        setKeyPressed(false);
      }
    },
    [targetKey]
  );

  useEffect(() => {
    window.addEventListener("keydown", handleKeyDown);
    window.addEventListener("keyup", handleKeyUp);

    return () => {
      window.removeEventListener("keydown", handleKeyDown);
      window.removeEventListener("keyup", handleKeyUp);
    };
  });
  return keyPressed;
};

이제 슬슬 커스텀훅 패턴이 보인다. 반복호출되는 함수들(이벤트 핸들러)등은 useCallback으로 감싸고 사용할 상태나 상태지정메서드를 리턴해준다.


useClickAway

타겟 외부 부분 클릭했을때 이벤트 호출! 모달창 끌때 사용하면 편리할거같다.

const events = ["mousedown", "touchstart"];

const useClickAway = (handler) => {
  const ref = useRef(null);
  const savedHandler = useRef(handler);

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const element = ref.current;
    if (!element) {
      return;
    }

    const handleEvent = (e) => {
      !element.contains(e.target) && savedHandler.current(e);
    };

    for (const eventName of events) {
      document.addEventListener(eventName, handleEvent);
    }

    return () => {
      for (const eventName of events) {
        document.removeEventListener(eventName, handleEvent);
      }
    };
  }, [ref]);

  return ref;
};

useEffect가 2번쓰인이유는 ref가변경됐을때와 handler가 변경되었을때를 나눠준다.(useEffect의 의존성 배열)

handler가 변경되었을때 이벤트를 document에 붙였다, 뗏다 하는 과정을 더 최적화 한 것임.


useReisze

노드의 크기가 변경될때마다 값을 얻어오는 훅. 이미지를 컨테이너 높이에 딱 맞게 설정할때도 유용하다.

const useResize = (handler) => {
  const savedHandler = useRef(handler);
  const ref = useRef(null);

  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const element = ref.current;
    if (!element) {
      return;
    }

    const observer = new ResizeObserver((entries) => {
      savedHandler.current(entries[0].contentRect);
    });

    observer.observe(element);

    return () => {
      observer.disconnect();
    };
  }, [ref]);

  return ref;
};

ResizeObserver는 특정 요소의 크기변화를 감지한다.
ref로 취득한 노드를 observer.observe에 전달함으로서 요소를 특정함.


useLocalStorage, useSessionStorage

강사님은 분리해서 만드셨지만, 이름만 다르기에 나는 공통 커스텀 훅인 useStorage로 만들었다...!

const useStorage = (key, initialValue, storage = localStorage) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = storage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore =
        typeof value === "function" ? value(storedValue) : value;
      setStoredValue(valueToStore);
      storage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };

  return [storedValue, setValue];
};

잘 작동하는구먼


느낀점

커스텀훅을 만들며 느낀점은 함수형 프로그래밍을 빨리 배워야 할것 같다. 리액트 자체도 함수 컴포넌트고 훅도 함수고 vue의 compositionAPI도 함수고...세상 모든게 함수다!

profile
모르는 것을 모른다고 하기

0개의 댓글