useHover hook 만들기 (+useRef, typescript)

D uuu·2023년 11월 25일

React

목록 보기
4/10

만들려고 하는 것

사진으로는 잘 안보이지만, 아이콘에 커서를 갖다대면 '예약 확인 중 입니다' 라는 문구가 나온다. 생각해보니 이 기능이 앞으로 여러군데 쓰일 것 같아서 이참에 hook 으로 만들어야겠다는 생각이 들었다.




어떻게 동작하는 거지?

내가 직접 고민해서 hook 을 만들어 보는게 제일 좋지만, 어떻게 만들어야 할지 감이 전혀 없어서 구글에 useHover 를 검색해봤다. 그러자 아래 코드를 많이 사용하는 듯 보였다. 근데 봐도봐도 이게 어떻게 동작하는거지?? 라는 생각이 들어 하나씩 뜯어보며 공부해보기로 했다.


<useHook 컴포넌트>

import { useState, useRef, useEffect } from "react";

export const useHover = () => {
  const [isHover, setIsHover] = useState(false);
  const ref = useRef(null);

  const handleMouseOver = () => setIsHover(true);
  const handleMouseOut = () => setIsHover(false);

  useEffect(() => {
    const element = ref.current;
    if (element) {
      element.addEventListener("mouseover", handleMouseOver);
      element.addEventListener("mouseout", handleMouseOut);
    }

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

  return { ref, isHover };
};

사용할때는 아래와 같이 가져다 쓰면 된다.

const Box = styled.div`
  width: 100px;
  height: 100px;
  background-color: red;
`;

export const Default = () => {
  const {ref, isHover} = useHover();

  return (
    <>
      <Box ref={ref} />
      {isHover ? <div>ToolTip!</div> : null}
    </>
  );
};

출처: https://ghost4551.tistory.com/178 [프론트엔드 개발자의 기록 공간:티스토리]
  1. 훅에서 반환하는 ref 를 받아와서

  2. DOM 요소 (여기서는 <Box> styled-component) 에 연결시켜주면,

  3. 해당 DOM 의 mouseover 하는 경우, DOM 요소에 진입했다고 판단하여 isHover 를 treu 로 DOM 요소를 벗어나면 false 를 준다.

  4. 그럼 훅에서 반환하는 isHover 를 받아와 true 일때의 원하는 텍스트를 보여준다.

useHover 의 핵심 동작은 저러한데, 위 코드에서 이해가 안갔던 부분이 바로 useRef 의 역할이다. 굉장히 단순한 코드인데 어떻게 동작하는건지 이해가 가지 않았다.

여기서 내가 useRef 에 대해 잘 모르고 있다는 생각이 들어 이번 기회에 useRef 에 대해 자세히 찾아보게 되었다.



useRef 란?

useRef 는 매번 사용할때마다 폭풍 검색을 하게 만드는 녀석이다😂 잘쓰면 매우 유용한 react hook 같은데, useState 보다 사용하기가 다소 까다롭다고 느껴져 사실 기피했던 경향이 있었다. 언젠가는 한번 공부를 해봐야 겠다는 생각이 있었는데 이번 기회에 useRef 를 공부해봤다.

useRef 의 대표적인 활용 방법은 아래와 같다.

  1. 로컬 변수 (리렌더링 방지)
  2. 특정 DOM 에 접근하기

로컬 변수 (리렌더링 방지)

useRef는 렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook입니다.

React 는 자주 바뀌는 값을 state 로 정의하고 state 가 변할때마다 알아서 렌더링을 하여 항상 우리는 새로운 state 를 볼 수 있는 것이 기본적인 리액트의 흐름이다.

그러나 useRef는 초깃값으로 설정된 값을 useRef 객체의 .current 프로퍼티에 저장하고, 그 값은 변경해도 재렌더링이 발생하지 않아 항상 같은 값을 유지할 수 있게 해주는 hook 이다.

따라서 이러한 useRef 의 기능을 이용해 로컬 변수 용도로 사용할 수 있다.

간단한 예시를 살펴보자.

function App() {
  const [state, setState] = useState(0);
  const obj = { num: 0 };
  let num = 0;

  const addNumHandler = () => {
    num = num + 1;
    obj.num = obj.num + 1;
    setState(state + 1);
  };

  return (
    <div className="App">
      <p>stateNum : {state}</p>
      <p>obj.num : {obj.num}</p>
      <p>local num : {num}</p>
      <button onClick={addNumHandler}>클릭</button>
    </div>
  );
}

export default App;

위에 보면 state, obj, 변수 num 에 초깃값을 0 으로 주었다. 그리고 버튼을 클릭하면 각각 +1 을 하도록 했다.

그런데 화면에는 stateNum 만 +1 된 값이 출력 되고, 나머지 두개는 계속 0 만 나온다.

그 이유는 버튼을 눌러 state 값이 1씩 증가할때마다 변화를 감지하고 렌더링 되어 변수 값이 초기화되기 때문이다.


function App() {
  const [state, setState] = useState(0);

  const obj = { num: 0 };
  let num = 0;
  const refNum = useRef(0); 

  const addNumHandler = () => {
    num += 1;
    obj.num += 1;
    refNum.current += 1;
    setState(state + 1);
  };

  return (
    <div className="App">
      <p>stateNum : {state}</p>
      <p>obj.num : {obj.num}</p>
      <p>local num : {num}</p>
      <p>useRef num : {refNum.current}</p>
      <button onClick={addNumHandler}>클릭</button>
    </div>
  );
}

export default App;

이번에는 useRef 를 사용해 refNum.current 프로퍼티에 +1 하는 로직을 한 줄 추가했다.
그랬더니, state 가 변하여 컴포넌트가 렌더링 되어도 useRef 값은 최신값을 유지하고 있는 걸 확인할 수 있다.

그 이유는 useRef.current 프로퍼티에 값을 담고 있기 때문이다. 따라서 state 가 바뀌어 컴포넌트가 렌더링 되어도 useRef.current 의 값은 유지되고 있는 것이다.

언제 쓰면 좋을까?

  • 최신 값을 유지하되 값이 변해도 렌더링 될 필요가 없을때.
  • 변하는 값을 화면에 즉각적으로 보여줘야 할때는 useState를..
    내부에서 참조해야 하는 변수는 useRef 를..

위 내용이 모두 정답은 아니지만, 보통 이럴때 useRef 를 로컬 변수로 만들어 사용하면 매우 유용할 것 같다.

그리고 대표적으로는 setInterval 를 사용할때 반환하는 id 를 useRef 에 저장한 다음 clear 할때 많이 사용하는 듯하다.


특정 DOM 에 접근하기

리액트로 작업할때 div 등의 DOM 요소에 id, onClick 등을 적용하기도 한다. 이렇게 하면 특정 id 에 해당하는 DOM 요소에만 스타일을 따로 적용시키거나 onClick 등 이벤트리스너를 주어 해당 DOM 요소를 클릭하였을때 어떠한 행동이 일어나도록 설정할 수 있다.

이처럼 특정 DOM 에 접근해서 무언가를 해야할때도 useRef 로 간단하게 처리할 수 있다.

보통 input 창을 마우스로 클릭하지 않아도 바로 키보드로 입력할 수 있도록 useRef 를 이용해 focus 처리를 하는 예시가 가장 흔하다.

function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

언제 쓰면 좋을까?

나는 단순히 input 에 입력한 값을 가져올때 state 와 onChange 를 사용하지 않고, useRef 를 이용해 가져오는 방법을 주로 사용했다.

onChange, useState 로 input 에 입력한 값을 가져올때 콘솔에 찍어보면 ㅎ,ㅗ,ㅇ,ㄱ,ㅣ,ㄹ,ㄷ,ㅗ,ㅇ 이렇게 자음, 모음이 입력될 때마다 렌더링 되는거에 비해 useRef 를 사용하면 홍길동 한 단어만 콘솔에 찍히는 걸 확인 할 수 있다.

이렇게 하면 렌더링 수를 줄일 수 있고 input 창에 직접 접근하여 .current.value 를 통해 값을 가져올 수 있어서 편리하다. 별도의 onChange 함수 등을 작성할 필요가 없다.

이처럼 state 값이 바뀔 때마다 변화를 감지하고 그에 맞는 액션을 취해야 할때는 useState 를 사용해서 렌더링이 되도록 유도해야 하지만, 굳이 필요 없는 상황에서는 useRef 를 잘 활용하면 될 것 같다.

function App() {
  const inputRef = useRef<HTMLInputElement | null>(null);

  const onsubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const obj = {
      name: inputRef.current?.value,
    };
    console.log(obj);
  };

  return (
    <form className="App" style={{ padding: "1rem" }} onSubmit={onsubmit}>
      <label htmlFor="name">이름</label>
      <input type="text" ref={inputRef} name="name" />
    </form>
  );
}

export default App;



다시 돌아와서

위에서 useRef 를 공부하고 다시 코드를 살펴보니 이제 이해가 가기 시작했다. useRef 의 DOM 요소에 직접 접근 할 수 있는 기능을 이용해서 useHover 훅을 만들었던 것이다.

const MyOrderSummary = () => {

  const { ref, isHover } = useHover();


  return (
    <div>
    
   .... 생략
  
          <InformationBoxStyle ref={ref}>
            <FcInfo />
            {isHover && <span>예약 확인 중 입니다.</span>}
          </InformationBoxStyle>
     
    </div>
  );
};

export default MyOrderSummary;

나는 icon 위에 hover 효과를 주고 싶어서 div 태그 안에 아이콘 <FcInfo> 을 넣어주고 div 태그에 ref 속성을 주어 mouseover 했을때 isHover 가 true 가 되어 '예약 확인 중 입니다.' 라는 문구가 나오도록 했다.

느낀점

지금까지 useRef 를 사용하지 않았던 것도 아니고, input 에 ref 를 주어 값을 가져와 사용했던 방법과 사실상 별반 다르지 않았는데, useRef 에 대해 제대로 알고 쓰질 않았기 때문에 이번에 useHover 코드를 봤을때 작동방법에 대해 이해하지 못했던 것 같다.

뭐든지 핵심 역할을 제대로 이해하고 사용하는 것이 중요하다고 다시금 느끼게 되었다.

그리고 typescript 를 사용했기 때문에 useRef 를 쓰면서 애를 많이 먹었는데, type 공부도 더 많이 필요하다고 느꼈다. 특히 type 관련해서는 아래 글에서 정말 많은 도움을 받았다. 더 읽어보고 따라해보고 공부해봐야겠다.

https://driip.me/7126d5d5-1937-44a8-98ed-f9065a7c35b5


도움 받은 글들
https://driip.me/7126d5d5-1937-44a8-98ed-f9065a7c35b5
https://chanhuiseok.github.io/posts/react-7/

profile
배우고 느낀 걸 기록하는 공간

0개의 댓글