
웹 애플리케이션에서 제한된 공간에 긴 텍스트를 표시할 때, 일반적으로 말줄임표(...)와 툴팁을 함께 사용합니다.
하지만 텍스트 길이가 동적이거나 반응형 레이아웃으로 컨테이너 크기가 변경되면, 단순한 구현으로는 한계가 있습니다.
이번 포스트에서는 텍스트가 지정된 maxWidth를 초과할 때만 자동으로 툴팁을 표시하는 스마트 컴포넌트를 구현하고,
추가로 renderLabel prop을 통해 label의 렌더링 방식을 직접 커스터마이징할 수 있도록 개선한 예제를 소개합니다.
maxWidth를 초과하는 텍스트에만 툴팁 표시 top, bottom, left, right 중 선택 가능 renderLabel prop을 통해 label에 직접 ellipsis 스타일, ref, 이벤트 핸들러 등을 할당하여 원하는 방식으로 렌더링 가능import React, { useState, useRef, useEffect, useMemo } from 'react';
interface SmartTooltipProps {
title: React.ReactNode;
maxWidth?: number;
children: React.ReactNode;
placement?: 'top' | 'bottom' | 'left' | 'right';
/**
* 커스텀 label 렌더링 함수.
* 기본적으로 ellipsis 스타일, ref, 이벤트 핸들러 등이 할당된 props를 인자로 전달합니다.
*/
renderLabel?: (props: {
ref: React.Ref<HTMLDivElement>;
className: string;
style: React.CSSProperties;
onMouseEnter: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave: React.MouseEventHandler<HTMLDivElement>;
children: React.ReactNode;
}) => React.ReactNode;
}
const SmartTooltip = ({
title,
maxWidth = 200,
children,
placement = 'top',
renderLabel,
}: SmartTooltipProps) => {
const [isOverflowing, setIsOverflowing] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const checkOverflow = () => {
if (contentRef.current) {
const element = contentRef.current;
const isElementOverflowing = element.scrollWidth > element.clientWidth;
setIsOverflowing(isElementOverflowing);
}
};
checkOverflow();
const resizeObserver = new ResizeObserver(checkOverflow);
if (contentRef.current) {
resizeObserver.observe(contentRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, [children]);
const tooltipStyle = useMemo(() => {
let positionClasses = 'bottom-full left-1/2 -translate-x-1/2 -translate-y-2';
let arrowClasses = 'top-full left-1/2 -translate-x-1/2 border-t-gray-800';
if (placement === 'bottom') {
positionClasses = 'top-full left-1/2 -translate-x-1/2 translate-y-2';
arrowClasses = 'bottom-full left-1/2 -translate-x-1/2 rotate-180 border-t-gray-800';
} else if (placement === 'left') {
positionClasses = 'right-full top-1/2 -translate-y-1/2 -translate-x-2';
arrowClasses = 'left-full top-1/2 -translate-y-1/2 -rotate-90 border-t-gray-800';
} else if (placement === 'right') {
positionClasses = 'left-full top-1/2 -translate-y-1/2 translate-x-2';
arrowClasses = 'right-full top-1/2 -translate-y-1/2 rotate-90 border-t-gray-800';
}
return { positionClasses, arrowClasses };
}, [placement]);
// 기본 label 렌더링에 필요한 props
const labelProps = {
ref: contentRef,
className: 'truncate',
style: { maxWidth: `${maxWidth}px` },
onMouseEnter: () => isOverflowing && setShowTooltip(true),
onMouseLeave: () => setShowTooltip(false),
children,
};
return (
<div className="relative inline-block">
{renderLabel ? (
// renderLabel 함수를 통해 커스텀 렌더링
renderLabel(labelProps)
) : (
// 기본 label 렌더링
<div
ref={contentRef}
className="truncate"
style={{ maxWidth: `${maxWidth}px` }}
onMouseEnter={() => isOverflowing && setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
{children}
</div>
)}
{showTooltip && isOverflowing && (
<div className={`absolute z-50 ${tooltipStyle.positionClasses}`} role="tooltip">
<div className="bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg max-w-xs">
{title}
</div>
<div className={`absolute w-0 h-0 border-4 border-transparent ${tooltipStyle.arrowClasses}`} />
</div>
)}
</div>
);
};
export default SmartTooltip;
interface SmartTooltipProps {
title: React.ReactNode; // 툴팁에 표시될 내용
maxWidth?: number; // 최대 너비
children: React.ReactNode; // 실제 표시될 내용
placement?: 'top' | 'bottom' | 'left' | 'right'; // 툴팁 위치
renderLabel?: (props: { // 커스텀 label 렌더링 함수
ref: React.Ref<HTMLDivElement>;
className: string;
style: React.CSSProperties;
onMouseEnter: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave: React.MouseEventHandler<HTMLDivElement>;
children: React.ReactNode;
}) => React.ReactNode;
}
renderLabel prop을 통해 label 렌더링 방식을 사용자 정의할 수 있음 useEffect(() => {
const checkOverflow = () => {
if (contentRef.current) {
const element = contentRef.current;
const isElementOverflowing = element.scrollWidth > element.clientWidth;
setIsOverflowing(isElementOverflowing);
}
};
checkOverflow();
const resizeObserver = new ResizeObserver(checkOverflow);
if (contentRef.current) {
resizeObserver.observe(contentRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, [children]);
scrollWidth와 clientWidth를 비교해 오버플로우 여부를 판단합니다.ResizeObserver를 사용하여 컨테이너 크기 변경 시 자동으로 오버플로우 상태를 업데이트합니다.const tooltipStyle = useMemo(() => {
let positionClasses = 'bottom-full left-1/2 -translate-x-1/2 -translate-y-2';
let arrowClasses = 'top-full left-1/2 -translate-x-1/2 border-t-gray-800';
// placement 값에 따라 위치 및 화살표 클래스 변경
if (placement === 'bottom') { ... }
else if (placement === 'left') { ... }
else if (placement === 'right') { ... }
return { positionClasses, arrowClasses };
}, [placement]);
<div className="bg-gray-800 text-white px-4 py-2 rounded-lg shadow-lg max-w-xs">
{title}
</div>
<div className={`absolute w-0 h-0 border-4 border-transparent ${tooltipStyle.arrowClasses}`} />
renderLabel)labelProps 객체로 정의합니다.renderLabel prop이 전달되면, 이 함수에 labelProps를 전달하여 개발자가 직접 커스텀 레이블 컴포넌트를 반환할 수 있습니다.// 1. 기본 사용 (renderLabel 미사용)
<SmartTooltip title="툴팁 내용" maxWidth={200}>
<div className="bg-gray-100 p-2">
긴 텍스트 내용...
</div>
</SmartTooltip>
// 2. 다양한 방향 지원
<SmartTooltip title="오른쪽에 표시되는 툴팁" placement="right" maxWidth={200}>
<div className="bg-blue-100 p-2">
호버하면 오른쪽에 툴팁이 표시됩니다.
</div>
</SmartTooltip>
// 3. 복잡한 툴팁 내용
<SmartTooltip
title={
<div>
<div className="font-bold">제목</div>
<div>상세 설명...</div>
</div>
}
placement="bottom"
maxWidth={300}
>
<div className="bg-green-100 p-2">
호버하면 아래에 복잡한 툴팁이 표시됩니다.
</div>
</SmartTooltip>
// 4. 커스텀 label 렌더링 사용 (renderLabel prop 활용)
<SmartTooltip
title="커스텀 렌더링된 툴팁"
maxWidth={250}
renderLabel={({ ref, className, style, onMouseEnter, onMouseLeave, children }) => (
<span
ref={ref}
className={`${className} text-ellipsis text-sm font-medium text-gray-800`}
style={style}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</span>
)}
>
<span>
이 텍스트는 커스텀 label로 렌더링됩니다.
</span>
</SmartTooltip>
useMemo: 툴팁 위치 계산을 메모이제이션하여 불필요한 재계산 방지 ResizeObserver: 컨테이너 크기 변경 시 효율적으로 오버플로우 상태를 업데이트 이번에 구현한 스마트 툴팁 컴포넌트는 다음과 같은 장점이 있습니다:
이 컴포넌트를 활용하면, 텍스트 오버플로우를 자동 감지하여 필요할 때만 툴팁을 표시하고, label의 렌더링 방식을 자유롭게 커스터마이징할 수 있습니다.
Tailwind CSS의 유틸리티 클래스를 활용하면 일관된 디자인을 유지하면서도, 다양한 요구사항에 맞춰 UI를 쉽게 조정할 수 있습니다.
이와 같이 구현하면, 텍스트 오버플로우 상황에서도 깔끔하게 툴팁을 표시하고, 필요에 따라 label의 렌더링 방식을 직접 제어할 수 있어 더욱 유연한 UI 구성이 가능합니다.