React Tooltip 만들어보기

ReactuseRefrefdata-tooltiptsxjsxTooltip툴팁onMouseenteronMouseleaveonMousemoveaddEventListenerremoveEventListeneruseStateuseEffectTypescriptgetAttributequerySelectorAll
avatar
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를 사용하여 관리하였다.
topleft를 이용하여 툴팁이 출력될 좌표를 저장하고,
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가 없거나 datatext 요소가 비어있으면 null을 리턴 함으로서 아무 것도 출력하지 않는다.

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

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







- 컬렉션 아티클