• Feed
  • Explore
  • Ranking
/
/
    React

    React Tooltip 만들어보기

    ReactuseRefrefdata-tooltiptsxjsxTooltip툴팁onMouseenteronMouseleaveonMousemoveaddEventListenerremoveEventListeneruseStateuseEffectTypescriptgetAttributequerySelectorAll
    전
    전상욱
    2025.04.10
    ·
    7 min read

    툴팁 요소 선정

    툴팁을 토글하기 위해 요소를 선택할 필요가 있었다.
    첫 번째로 생각한건 useRef를 사용하여 요소를 선택하는 것이였는데 useRef를 사용하기 위해 툴팁이 필요한 요소에 모두 ref 속성을 넣어주기는 사용하기도 불편했고 ref 값에 배열로 만들어 처리하는 것도 그렇게 크게 좋은 것 같지 않았다.
    그래서 선택한 방법은 태그 속성으로 data-tooltip에 툴팁에 나타낼 텍스트를 적어주고 data-tooltip 속성이 존재하는 요소를 모두 선택하게 되었다.

    툴팁이 필요한 요소에 data-tooltip 속성을 넣어줌으로서 Tooltip.tsx 컴포넌트에서 일괄적으로 선택이 가능해졌다.

    아래와 같이 사용할 예정이다.

    export default function App() {
      return (
        <>
          <div data-tooltip="첫 번째 툴팁">툴팁1</div>
          <div data-tooltip="두 번째 툴팁">툴팁2</div>
          <div data-tooltip="세 번째 툴팁">툴팁3</div>
        </>
      );
    }

    툴팁 컴포넌트 만들기

    전체 코드

    Tooltip.tsx

    import { useEffect, useState } from "react";
    
    interface IData {
      top: number;
      left: number;
      text: string;
    }
    
    export default () => {
      const [data, setData] = useState<null | IData>(null);
    
      const onMouseenter = ({ currentTarget, pageX, pageY }: MouseEvent) => {
        if (!currentTarget) return;
        const text = (currentTarget as Element)?.getAttribute("data-tooltip") ?? "";
        setData({ text, left: pageX, top: pageY });
      };
      const onMouseleave = () => {
        setData(null);
      };
      const onMousemove = ({ pageX, pageY }: MouseEvent) => {
        setData((x) => ({ ...x!, left: pageX, top: pageY }));
      };
    
      useEffect(() => {
        let els: NodeListOf<HTMLElement> =
          document.querySelectorAll("[data-tooltip]");
        els?.forEach((el) => {
          el.addEventListener("mouseenter", onMouseenter);
          el.addEventListener("mouseleave", onMouseleave);
          el.addEventListener("mousemove", onMousemove);
        });
    
        return () => {
          els?.forEach((el) => {
            el?.removeEventListener("mouseenter", onMouseenter);
            el?.removeEventListener("mouseleave", onMouseleave);
            el?.removeEventListener("mousemove", onMousemove);
          });
        };
      }, []);
    
      if (!data || !data.text) return null;
      return (
        <div
          style={{
            position: "fixed",
            zIndex: 2,
            backgroundColor: "#f00",
            color: "#fff",
            padding: 5,
            left: data.left + 10,
            top: data.top + 5,
          }}
        >
          {data.text}
        </div>
      );
    };

    일부 코드 설명

    데이터 관리
    Tooltip.tsx

    interface IData {
      top: number;
      left: number;
     text: string;
    }
    const [data, setData] = useState<null | IData>(null);

    툴팁에 필요한 필수 데이터를 담기 위해 useState를 사용하여 관리하였다.
    top과 left를 이용하여 툴팁이 출력될 좌표를 저장하고,
    text를 이용하여 툴팁에 나타날 텍스트를 저장하였다.

    함수
    Tooltip.tsx

    const onMouseenter = (e: MouseEvent) => {
      const { currentTarget, pageX, pageY } = e;
      if (!currentTarget) return;
      const text = (currentTarget as Element)?.getAttribute("data-tooltip") ?? "";
      setData({ text, left: pageX, top: pageY });
    };
    const onMouseleave = () => {
      setData(null);
    };
    const onMousemove = ({ pageX, pageY }: MouseEvent) => {
      setData((x) => ({ ...x!, left: pageX, top: pageY }));
    };

    onMouseenter 함수는 툴팁이 필요한 요소에 마우스 커서를 올렸을 때 툴팁에 필요한 정보를 수집하는 함수이다.
    1. 이벤트 객체의 이벤트 발생 타겟과 현재 마우스 커서의 X축 좌표, Y축 좌표값을 가져온다.
    2. 이벤트 발생 타켓의 data-tooltip 속성값을 가져와 툴팁 데이터 (useState)에 text와 X, Y축의 좌표값을 모두 넣어 저장한다.

    onMouseleave 함수는 툴팁이 필요한 요소에서 마우스 커서를 나갔을 때 툴팁 정보를 지우는 함수이다.

    onMousemove 함수는 마우스가 커서가 툴팁에서 나가지 않았다면 요소 내부에서 커서를 지속적으로 따라다녀 툴팁이 지속적으로 움직일 수 있도록 하는 함수이다.
    1. 커서가 올라가있는 요소에서 마우스를 움직일 때 호출된다. 움직일 때 마다 커서의 위치값을 저장한다.
    2. 여기 함수에서 따로 data가 없을 경우를 처리하지 않은 이유는 컴포넌트의 JSX 리턴 부분에서 data가 없으면 null을 리턴하기 때문에 요소가 없어지기 때문에 mousemove 함수를 호출 할 수가 없어서 따로 처리하지 않았다.

    이벤트 리스너 등록
    Tooltip.tsx

      ...
      useEffect(() => {
        let els: NodeListOf<HTMLElement> =
          document.querySelectorAll("[data-tooltip]");
        els?.forEach((el) => {
          el.addEventListener("mouseenter", onMouseenter);
          el.addEventListener("mouseleave", onMouseleave);
          el.addEventListener("mousemove", onMousemove);
        });
        return () => {
          els?.forEach((el) => {
            el?.removeEventListener("mouseenter", onMouseenter);
            el?.removeEventListener("mouseleave", onMouseleave);
            el?.removeEventListener("mousemove", onMousemove);
          });
        };
      }, []);
      ...

    랜더링 직후 data-tooltip이 존재하는 요소 전부를 가져와 각각 mouseenter, mouseleave, mousemove 이벤트를 등록 시켜 주었다.
    컴포넌트가 언마운트 될 때 각 이벤트 등록을 지워주었다.

    JSX
    Tooltip.tsx

      ...
      if (!data || !data.text) return null;
      return (
        <div
          style={{
            position: "fixed",
            zIndex: 2,
            backgroundColor: "#f00",
            color: "#fff",
            padding: 5,
            left: data.left + 10,
            top: data.top + 5,
          }}
    
          {data.text}
        </div>
      );
      ...
    1. data가 없거나 data의 text 요소가 비어있으면 null을 리턴 함으로서 아무 것도 출력하지 않는다.

    2. 툴팁이 출력할 좌표값을 각각 left, top에 등록 시켜주었고 +10, +5 씩 해준 이유는 툴팁이 마우스 커서에 조금 가려질 수 있기 때문에 약간 옆에서 출력하도록 하였다.

    3. 툴팁의 text는 툴팁의 내용으로 출력할 수 있도록 하였다.







    - 컬렉션 아티클