[ 기술 UX ] 텍스트 오버플로우 자동 감지 툴팁 구현하기
안녕하세요. 오늘은 Vue3 프로젝트에서 텍스트가 잘렸을 때만 자동으로 툴팁을 표시하는 기능을 구현한 경험을 공유하고자 합니다.
🤔 문제 상황: 기존 툴팁의 한계
프로젝트를 진행하면서 사용자들로부터 이런 피드백을 받았습니다:
"텍스트가 ...로 잘려있는데 전체 내용을 볼 방법이 없나요?"
처음에는 몇 가지 방법을 검토해보았습니다:
1. HTML title 속성
<input title="아주 긴 텍스트입니다..." value="아주 긴 텍스트..." />
2. CSS data-tooltip 방식
<div data-tooltip="툴팁 내용" class="tooltip-container">
긴 텍스트가 들어갈 요소
</div>
.tooltip-container {
position: relative;
}
.tooltip-container::before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 8px;
border-radius: 4px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.tooltip-container:hover::before {
opacity: 1;
}
하지만 각각의 한계점들을 발견했습니다:
기존 방식들의 문제점
1. HTML title 속성
- 텍스트가 잘리지 않았어도 hover 시 툴팁이 나타남
- 브라우저마다 다른 디자인으로 일관성 없음
- 타이밍과 애니메이션 제어 불가능
2. CSS data-tooltip 방식
- 순수 CSS로는 "텍스트가 잘렸는지" 감지 불가능
- JavaScript 없이는 조건부 표시 어려움
- 동적으로 data-tooltip 속성을 추가/제거해야 하는 번거로움
- 복잡한 위치 계산과 반응형 대응 한계
3. 동적 상황 대응의 어려움
- 창 크기 변경, 텍스트 변경 시 수동으로 관리해야 함
- 실시간 반응성 부족
💭 고민 과정: 어떻게 해결할까?
핵심 요구사항 정의
이 문제를 해결하기 위해 먼저 요구사항을 명확히 했습니다:
- 조건부 표시: 텍스트가 실제로 잘렸을 때만 툴팁 표시
- 자동 감지: 창 크기 변경, 텍스트 변경 시 자동으로 상태 업데이트
- 접근성: 마우스뿐만 아니라 키보드 사용자도 고려
- 일관된 디자인: 프로젝트의 디자인 시스템과 일치
기술적 고려사항
텍스트 오버플로우 감지 방법
// 핵심 아이디어: scrollWidth vs offsetWidth 비교
const isTextTruncated = (element: HTMLElement): boolean => {
return element.scrollWidth > element.offsetWidth;
};
실시간 감지 방법 검토
- window.resize 이벤트: 창 크기 변경만 감지 가능, 성능 이슈
- MutationObserver: DOM 변화 감지하지만 크기 변화는 직접적으로 감지 어려움
- ResizeObserver: 요소의 크기 변화를 정확히 감지 (최종 채택!)
왜 data-tooltip 방식을 포기했나?
data-tooltip + CSS 방식도 고려했지만 핵심 문제가 있었습니다:
/* CSS만으로는 텍스트 오버플로우 감지 불가능 */
.text:hover::before {
content: attr(data-tooltip);
/* 텍스트가 잘렸는지 모르니까 항상 표시됨 */
}
결국 JavaScript로 동적 제어가 필요한데, 그렇다면 처음부터 완전한 커스텀 구현이 더 나은 선택이었습니다.
🎯 해결 방법: 커스텀 툴팁 시스템 구현
1. 텍스트 오버플로우 감지 로직
// 텍스트가 잘렸는지 확인하는 핵심 함수
const checkTextOverflow = (element: HTMLElement | null): boolean => {
if (!element) return false;
// scrollWidth: 실제 콘텐츠 너비
// offsetWidth: 화면에 표시되는 너비
return element.scrollWidth > element.offsetWidth ||
element.scrollWidth > element.clientWidth;
};
2. 실시간 감지 시스템
ResizeObserver를 활용해 요소 크기 변화를 실시간으로 감지합니다:
const setupResizeObserver = () => {
// 브라우저 지원 여부 확인 (IE는 미지원)
if (typeof ResizeObserver === 'undefined') {
return;
}
resizeObserver = new ResizeObserver(() => {
// 요소 크기 변화 시 툴팁 상태 재확인
updateTooltipState();
});
// 감지할 요소 등록
if (textElement.value) {
resizeObserver.observe(textElement.value);
}
};
const updateTooltipState = () => {
// DOM 업데이트 완료 후 실행
setTimeout(() => {
if (textElement.value) {
isTextTruncated.value = checkTextOverflow(textElement.value);
}
}, 10);
};
3. 상호작용 로직 구현
마우스 호버 처리
const onMouseEnter = (event: MouseEvent) => {
// 텍스트가 잘리지 않았거나 툴팁 비활성화된 경우 종료
if (!isTextTruncated.value || !showTooltipEnabled.value) {
return;
}
updateTooltipPosition(event);
// 자연스러운 UX를 위한 300ms 딜레이
tooltipTimer = window.setTimeout(() => {
showTooltip.value = true;
}, 300);
};
const onMouseLeave = () => {
if (tooltipTimer) {
clearTimeout(tooltipTimer);
tooltipTimer = null;
}
// 포커스 상태가 아닐 때만 숨김 (접근성 고려)
if (!isFocused.value) {
showTooltip.value = false;
}
};
키보드 접근성 지원
const onFocus = (event: FocusEvent) => {
isFocused.value = true;
// 텍스트가 잘렸고 readonly/disabled가 아닌 경우 툴팁 표시
if (isTextTruncated.value && showTooltipEnabled.value &&
!readonly.value && !disabled.value) {
updateTooltipPosition(undefined, textElement.value);
showTooltip.value = true;
}
};
const onBlur = (event: FocusEvent) => {
isFocused.value = false;
showTooltip.value = false;
};
4. 특수 상황 처리: readonly/disabled 상태
readonly나 disabled 상태의 input은 마우스 이벤트를 받지 못하므로 투명한 오버레이를 추가했습니다:
<template>
<div style="position: relative;">
<input
:readonly="readonly"
:disabled="disabled"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
/>
<!-- readonly/disabled일 때 이벤트를 받는 오버레이 -->
<div
v-if="(readonly || disabled) && showTooltipEnabled"
:style="{
position: 'absolute',
top: 0, left: 0, right: 0, bottom: 0,
pointerEvents: 'auto',
backgroundColor: 'transparent',
zIndex: 1
}"
@mouseenter="onOverlayMouseEnter"
@mouseleave="onOverlayMouseLeave"
/>
</div>
</template>
5. 툴팁 렌더링
Teleport를 사용해 body에 직접 렌더링하여 z-index 문제를 해결했습니다:
<template>
<!-- Teleport로 body에 직접 렌더링 -->
<Teleport to="body">
<div
v-show="isTextTruncated && showTooltip"
:style="{
position: 'fixed',
left: `${tooltipPosition.x}px`,
top: `${tooltipPosition.y}px`,
transform: 'translate(-50%, -100%)',
backgroundColor: 'var(--primary-color)',
color: 'white',
padding: '8px 12px',
borderRadius: '6px',
fontSize: '14px',
zIndex: 999999,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
pointerEvents: 'none',
maxWidth: '320px',
wordBreak: 'break-all',
whiteSpace: 'normal'
}"
>
{{ fullText }}
<!-- 화살표 -->
<div :style="{
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
width: 0,
height: 0,
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderTop: '6px solid var(--primary-color)'
}" />
</div>
</Teleport>
</template>
6. 메모리 관리
컴포넌트 언마운트 시 적절한 정리를 통해 메모리 누수를 방지했습니다:
onUnmounted(() => {
// ResizeObserver 정리
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
// 타이머 정리
if (tooltipTimer) {
clearTimeout(tooltipTimer);
}
});
📊 결과: 개선된 사용자 경험
Before & After 비교
항목 기존 방식 개선된 방식
표시 조건 | 항상 표시 | 텍스트가 잘렸을 때만 |
디자인 | 브라우저마다 다름 | 일관된 디자인 시스템 |
타이밍 | 즉시 또는 브라우저 설정 | 최적화된 300ms 딜레이 |
접근성 | 제한적 | 키보드 네비게이션 완벽 지원 |
동적 대응 | 수동 관리 필요 | 자동 감지 및 업데이트 |
성능 개선
- 불필요한 툴팁 제거: 텍스트가 잘리지 않은 경우 툴팁이 표시되지 않음
- 효율적인 감지: ResizeObserver로 정확한 시점에만 업데이트
- 메모리 최적화: 적절한 정리로 메모리 누수 방지
🔍 핵심 포인트와 주의사항
구현 시 중요한 포인트
- 타이밍 이슈: DOM 업데이트 완료 후 측정하도록 setTimeout 사용
- 브라우저 호환성: ResizeObserver 지원 여부 확인 필수
- 이벤트 충돌: readonly/disabled 상태의 특별 처리
- 접근성: 키보드 사용자도 고려한 포커스 관리
주의사항
- ResizeObserver는 IE에서 지원되지 않으므로 폴리필이나 대안 필요
- 툴팁이 화면 밖으로 나가지 않도록 위치 계산 시 경계 체크 권장
- 많은 요소에 적용 시 성능 영향 고려
🚀 향후 개선 방향
현재 구현도 만족스럽지만, 추가로 개선할 수 있는 부분들이 있습니다:
- 애니메이션 추가: fade-in/out 트랜지션으로 더 부드러운 UX
- 모바일 최적화: 터치 이벤트와 모바일 환경 고려
- 위치 자동 조정: 화면 경계 감지 후 자동으로 위치 조정
- 다국어 RTL 지원: 아랍어, 히브리어 등 우측에서 좌측 언어 지원
🎯 마무리
텍스트 오버플로우 자동 감지 툴팁 구현을 통해 단순해 보이는 기능 하나도 사용자 경험을 크게 개선할 수 있다는 것을 다시 한번 확인했습니다. 특히 "필요할 때만, 예쁘게, 자연스럽게"라는 원칙으로 접근한 결과, 사용자들의 만족도가 크게 향상되었습니다.
개선 아이디어가 있으시면 언제든 댓글로 공유해 주세요!