je개발 복습/기술 UX

[ 기술 UX ] 텍스트 오버플로우 자동 감지 툴팁 구현하기

Je-chan 2025. 6. 20. 16:01

안녕하세요. 오늘은 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. 동적 상황 대응의 어려움

  • 창 크기 변경, 텍스트 변경 시 수동으로 관리해야 함
  • 실시간 반응성 부족

💭 고민 과정: 어떻게 해결할까?

핵심 요구사항 정의

이 문제를 해결하기 위해 먼저 요구사항을 명확히 했습니다:

  1. 조건부 표시: 텍스트가 실제로 잘렸을 때만 툴팁 표시
  2. 자동 감지: 창 크기 변경, 텍스트 변경 시 자동으로 상태 업데이트
  3. 접근성: 마우스뿐만 아니라 키보드 사용자도 고려
  4. 일관된 디자인: 프로젝트의 디자인 시스템과 일치

 

기술적 고려사항

 

텍스트 오버플로우 감지 방법

// 핵심 아이디어: 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로 정확한 시점에만 업데이트
  • 메모리 최적화: 적절한 정리로 메모리 누수 방지

 

🔍 핵심 포인트와 주의사항

구현 시 중요한 포인트

  1. 타이밍 이슈: DOM 업데이트 완료 후 측정하도록 setTimeout 사용
  2. 브라우저 호환성: ResizeObserver 지원 여부 확인 필수
  3. 이벤트 충돌: readonly/disabled 상태의 특별 처리
  4. 접근성: 키보드 사용자도 고려한 포커스 관리

주의사항

  • ResizeObserver는 IE에서 지원되지 않으므로 폴리필이나 대안 필요
  • 툴팁이 화면 밖으로 나가지 않도록 위치 계산 시 경계 체크 권장
  • 많은 요소에 적용 시 성능 영향 고려

🚀 향후 개선 방향

현재 구현도 만족스럽지만, 추가로 개선할 수 있는 부분들이 있습니다:

  1. 애니메이션 추가: fade-in/out 트랜지션으로 더 부드러운 UX
  2. 모바일 최적화: 터치 이벤트와 모바일 환경 고려
  3. 위치 자동 조정: 화면 경계 감지 후 자동으로 위치 조정
  4. 다국어 RTL 지원: 아랍어, 히브리어 등 우측에서 좌측 언어 지원

🎯 마무리

텍스트 오버플로우 자동 감지 툴팁 구현을 통해 단순해 보이는 기능 하나도 사용자 경험을 크게 개선할 수 있다는 것을 다시 한번 확인했습니다. 특히 "필요할 때만, 예쁘게, 자연스럽게"라는 원칙으로 접근한 결과, 사용자들의 만족도가 크게 향상되었습니다.

 

개선 아이디어가 있으시면 언제든 댓글로 공유해 주세요!