이 글은 누구를 위한 것인가
- 툴팁과 팝오버의 차이를 모르고 혼용하는 팀
- 모바일에서 툴팁이 안 보이는 문제를 해결하고 싶은 개발자
- 복잡한 기능을 사용자에게 설명하는 도움말 UI를 설계하는 팀
들어가며
"이 버튼이 뭘 하는지" 궁금한 사용자에게 매뉴얼 링크를 주는 것은 실패다. 컨텍스트 바로 옆에서 간단하게 설명해주는 것이 핵심이다. 단, 모바일에서 호버가 없으므로 툴팁만으로는 부족하다.
이 글은 bluefoxdev.kr의 컨텍스트 도움말 UX 를 참고하여 작성했습니다.
1. 도움말 UI 유형 선택
[도움말 컴포넌트 선택 가이드]
툴팁 (Tooltip):
트리거: 호버 (마우스 올릴 때)
내용: 짧은 레이블 (1-2줄)
사라짐: 마우스 이동 시 자동
용도: 아이콘 버튼 레이블, 축약 텍스트 설명
❌ 중요한 정보는 넣지 말 것 (발견 어려움)
❌ 모바일에서 작동 안 함
팝오버 (Popover):
트리거: 클릭
내용: 긴 설명, 링크, 이미지 포함 가능
사라짐: 외부 클릭 또는 닫기 버튼
용도: 설정 설명, 용어 설명, 미리보기
✅ 모바일에서도 작동
인포 아이콘 + 팝오버:
패턴: ℹ 아이콘 → 클릭 → 팝오버
용도: 폼 필드 설명, 복잡한 개념 설명
최적: 한 화면에 3개 이하
코치마크 (Coachmark):
트리거: 첫 방문 또는 새 기능 출시
내용: 기능 하이라이트 + 설명
사라짐: 확인 클릭
용도: 온보딩, 신규 기능 안내
인라인 도움말:
항상 보임 (접기/펼치기 없음)
용도: 복잡한 입력 형식, 오류 설명
✅ 가장 접근하기 쉬움
[모바일 툴팁 대안]
정보 아이콘 (ℹ) + 탭 → 바텀시트로 설명
인라인 도움말 텍스트 (항상 표시)
Expandable 설명 (펼치기/닫기)
2. 위치 자동 계산 툴팁
import { useState, useRef, useEffect } from "react";
type TooltipPosition = "top" | "bottom" | "left" | "right";
interface TooltipProps {
content: string;
children: React.ReactNode;
position?: TooltipPosition;
delay?: number;
}
function Tooltip({ content, children, position = "top", delay = 300 }: TooltipProps) {
const [isVisible, setIsVisible] = useState(false);
const [actualPosition, setActualPosition] = useState<TooltipPosition>(position);
const triggerRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const timerRef = useRef<NodeJS.Timeout>();
const calculatePosition = () => {
if (!triggerRef.current || !tooltipRef.current) return;
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const viewport = { width: window.innerWidth, height: window.innerHeight };
let pos = position;
// 화면 밖으로 나가는지 확인하고 반전
if (pos === "top" && triggerRect.top < tooltipRect.height + 8) pos = "bottom";
if (pos === "bottom" && triggerRect.bottom + tooltipRect.height + 8 > viewport.height) pos = "top";
if (pos === "left" && triggerRect.left < tooltipRect.width + 8) pos = "right";
if (pos === "right" && triggerRect.right + tooltipRect.width + 8 > viewport.width) pos = "left";
setActualPosition(pos);
};
const positionStyles: Record<TooltipPosition, string> = {
top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
left: "right-full top-1/2 -translate-y-1/2 mr-2",
right: "left-full top-1/2 -translate-y-1/2 ml-2",
};
const arrowStyles: Record<TooltipPosition, string> = {
top: "top-full left-1/2 -translate-x-1/2 border-t-gray-800",
bottom: "bottom-full left-1/2 -translate-x-1/2 border-b-gray-800",
left: "left-full top-1/2 -translate-y-1/2 border-l-gray-800",
right: "right-full top-1/2 -translate-y-1/2 border-r-gray-800",
};
return (
<div
ref={triggerRef}
className="relative inline-flex"
onMouseEnter={() => {
timerRef.current = setTimeout(() => {
setIsVisible(true);
requestAnimationFrame(calculatePosition);
}, delay);
}}
onMouseLeave={() => {
clearTimeout(timerRef.current);
setIsVisible(false);
}}
// 접근성: 포커스에도 표시
onFocus={() => setIsVisible(true)}
onBlur={() => setIsVisible(false)}
>
{children}
{isVisible && (
<div
ref={tooltipRef}
role="tooltip"
className={`absolute z-50 px-3 py-1.5 text-xs text-white bg-gray-800 rounded-lg whitespace-nowrap pointer-events-none ${positionStyles[actualPosition]}`}
>
{content}
{/* 화살표 */}
<div
className={`absolute w-0 h-0 border-4 border-transparent ${arrowStyles[actualPosition]}`}
/>
</div>
)}
</div>
);
}
// 사용 예시
function IconButton({ icon, label, onClick }: IconButtonProps) {
return (
<Tooltip content={label} position="bottom">
<button
onClick={onClick}
aria-label={label}
className="p-2 rounded-lg hover:bg-gray-100"
>
{icon}
</button>
</Tooltip>
);
}
마무리
툴팁은 "모바일에서 작동하지 않는다"는 것을 항상 기억해야 한다. 모바일 사용자는 호버를 할 수 없다. 중요한 설명은 인라인 텍스트나 팝오버(클릭)로 처리하고, 툴팁은 데스크탑에서 아이콘 버튼 레이블 정도에만 쓰는 것이 안전하다.