React Tooltip 만들어보기
툴팁 요소 선정
툴팁을 토글하기 위해 요소를 선택할 필요가 있었다.
첫 번째로 생각한건 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> ); ...
data
가 없거나data
의text
요소가 비어있으면null
을 리턴 함으로서 아무 것도 출력하지 않는다.툴팁이 출력할 좌표값을 각각
left
,top
에 등록 시켜주었고+10
,+5
씩 해준 이유는 툴팁이 마우스 커서에 조금 가려질 수 있기 때문에 약간 옆에서 출력하도록 하였다.툴팁의
text
는 툴팁의 내용으로 출력할 수 있도록 하였다.