본문 바로가기

TECH/React

말줄임 + hover시 툴팁 표시 컴포넌트 만들기

이런 리스트에서 2줄이 되면서, 툴팁을 표시하는 컴포넌트를 만들 일이 생겼는데
생각보다 이렇게 복잡한 로직이었나? 되돌아보게 되어 정리해보았습니다.
( typescript, Next.js , styled-components를 이용했습니다.)

1. 2줄 말줄임 CSS

먼저, 2줄 말줄임에 대한 CSS 를 정의해주고 적용시켜줄 수 있습니다.

text-overflow:ellipsis;
overflow:hidden;
word-break:break-word ;
display: -webkit-box;
-webkit-line-clamp:2;
-webkit-box-orient:vertical;

- webkit-line-clamp : 원하는 줄의 단위를 선택할 수 있습니다.

2. 말줄임 확인

말줄임이 된 Item 과 말줄임이 되지 않은 Item에 따라 툴팁 노출 여부가 달라지기 때문에 말줄임 여부를 검사해주어야합니다.

또한, Hover 됐을 때 툴팁을 노출해야하므로 이와 관련된 모든 로직은 JS를 이용했습니다.

1 ) 해당 요소를 잡기

<li class="shortcut-test">요소1<li>

document.querySelector(".shortcut-test");

바닐라 자바스크립트와 같은 경우엔 document.querySelector를 이용해서 해당 요소를 잡았지만, React 기반의 프레임워크를 이용하려면 ref를 이용하여 dom 요소를 잡아주어야 합니다.

type Props = {
  text: string;
};

const 컴포넌트명 = ({ text }: Props)=>{
  const ref = useRef<HTMLDivElement>(null);
  ...
  return(
    <>
         <TextWrapper ref={ref}>
        {text}
        </TextWrapper>
    </>
  )
}

const TextWrapper = styled.div`
  text-overflow: ellipsis;
  overflow: hidden;
  word-break: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
`;

export default 컴포넌트명

2) 말줄임 체크

요소를 잡아주었으면, 해당 요소의 말줄임이 되었는지 체크가 필요합니다.
그에 대한 문제는 css 적용된 후 element 의 높이와 css가 적용되지 않은 element 의 높이의 차이로 구해주려고합니다.

 

- offsetHeight : 요소의 높이→ 패딩,테두리 포함 (마진 제외)
- scrollHeight : 컨텐츠 전체 높이 → 패딩, 테두리 포함 (마진 제외)

 

clientHeight도 존재하는데, 쓰지 않은 이유는 패딩값이 포함되어 원하는 결과값이 나오지 않을 수 있기 때문입니다. padding 값을 설정하지 않았다면, offsetHeight 대신 clientHeight를 사용해도 됩니다. 근데 스타일링이 변경될 수 있는 문제도 있기 때문에 굳이 ! 추천은 하고 싶지 않습니다.

 

말줄임이 되지 않은 원본 상태의 높이는 scrollHeight, 말줄임이 된 원본의 현재 높이는 offsetHeight 입니다.

 

MultiLine 일 때만 height 를 사용하고 1줄 말줄임이라면 offsetWidthscrollWidth 를 통해 구했습니다.

 

구구절절 설명했지만 원본 상태의 높이(scrollHeight)보다 말줄임이 된 현재의 높이(offsetHeight)가 높다면 말줄임 된 상태입니다 !

 

const 컴포넌트명 = ({ text }: Props) => {
  const [isActiveEllipsis, setIsActiveEllipsis] = useState(false);

  useEffect(() => {
        // typescript로 작성했기 때문에 초기 값 null이 들어올 수 있어 예외처리를 해준다.
    if (!ref || !ref.current) return;
    setIsActiveEllipsis(ref.current.scrollHeight > ref.current.offsetHeight);
  }, [text]);

  }

useEffect를 이용하여 dependancies에 text를 넣어주어 text가 변경될 때마다 말줄임이 되었는지 되지 않았는지 설정해주었습니다.

2) Hover 체크

이제 Element의 마우스가 Hover 됐는지 체크해주는 단계만 남았습니다.
Hover에 대한 부분은 CSS가 아니라 JS 이벤트를 활용했습니다.

처음에는 onMouseOveronMouseOut를 통해 hover 이벤트를 캐치해주었습니다. 그러나 해당 Element에서 마우스를 움직이면 이벤트가 반복적으로 호출되는 문제가 발생해서 OnMouseEnteronMouseLeave로 체크해주는걸로 바꿔주었습니다 !

 

2.  툴팁 노출

마우스 위치에 맞게 툴팁 노출하고자 했습니다.

1) 마우스 위치 계산

마우스 이벤트의 e.pageX와 e.pageY를 활용하면 현재 마우스의 Location을 구할 수 있습니다.

툴팁에 해당 마우스 위치를 주입 시켜주어야 하고, 해당 Element 에서 Hover 이벤트가 없어지면, 마우스 위치를 넣어주지 않아도 됩니다. 그래서, state로 관리했습니다 !

 

const 컴포넌트명 = ({ text }: Props) => {
  const [tooltipLocation, setTooltipLocation] = useState<null | CSSProperties>(
    null
  );
  const hanldeMouseEnterEvent = (
    e: React.MouseEvent<HTMLDivElement, MouseEvent>
  ) => {
    if (!isActiveEllipsis) return;

    setTooltipLocation({ left: e.pageX, top: e.pageY });
  };

  const handleMouseLeaveEvent = () => {
  	// Element 에서 hover 되지 않으면 style 주입하지 않음
    if (!isActiveEllipsis) return;

    setTooltipLocation(null);
  };
  
    return (
    <>
      // 말줄임 + element Hover 되어있으면 tooltip 노출
      {isActiveEllipsis && tooltipLocation && (
        <TooltipWrapper style={tooltipLocation}>{text}</TooltipWrapper>
      )}
      <TextWrapper
        onMouseEnter={(e) => hanldeMouseEnterEvent(e)}
        onMouseLeave={handleMouseLeaveEvent}
      >
        {text}
      </TextWrapper>
     </>
  );
};


const TooltipWrapper = styled.div`
  width: 700px;
  background: rgba(0, 0, 0, 0.8);
  border-radius: 10px;
  padding: 15px 12px;
  color: #fff;
  position: fixed;
`;

 

번외 ) createProtal ?

modal 을 만들면서 createPortal 이라는 기능을 알게 되었습니다. '이걸로도 툴팁을 만들어야하나? -> 불필요할 것 같다' 라는 생각이 들어서 결국 적용은 하지 않았지만 새롭게 배운 기능이라 작성하게 되었습니다 !

 

[React]Portal 사용하는 이유와 방법

React의 createPortal

 

 

createPortal을 이용하면, 선언된 컴포넌트 내부가 아니라 바깥쪽으로 이동시켜주게 됩니다. (body나 root 모두 가능 !) Modal 혹은 Dialog 와 같이 페이지의 상단에 띄워야할 때 유용하다고 합니다.

<div>
  <SomeComponent />
  {createPortal(children, domNode, key?)}
</div>
  • children : JSX 혹은 string, number 등
  • domNode : 미리 생성되어있는 Node입니다. 해당 Dom Node 자식으로 children Element가 들어갑니다.
  • Key(Optional) : 리액트에서 반복문 돌 때 설정해주는 unique한 Key 값과 동일합니다.

 

혹시 툴팁에도 적용이 가능하다 싶으시다면 ! createPortal을 활용해보시는 것도 좋을 것 같습니다.