React hover event를 활용한 별점 평점 만들기 (MouseEnter, MouseLeave)

y___h·2023년 4월 5일
0

영화 평점&리뷰 웹 사이트 프로젝트 시 구현했던 별점 평점 구현 과정에 대해 기록해두려 한다.

기능 및 구현 결과

  • Mouse Cursor 위치에 따라 별점 색상 변경
  • 별 클릭 시 별점 선택
  • 단위는 0.5, 총 5점


React엔 hover가 없는데요?

hover란, MDN 정의에 의하면 유저가 마우스 커서를 어떠한 요소에 올릴 때, 선택되는 CSS 선택자를 의미한다.

The :hover CSS pseudo-class matches when the user interacts with an element with a pointing device, but does not necessarily activate it. It is generally triggered when the user hovers over an element with the cursor (mouse pointer).

우리가 알고 있는 CSS hover 선택자는 마우스 커서가 해당 요소에 위치하면, 선택된다. 즉, 커서가 해당 요소에서 벗어나면 선택 해제 된다.

하지만 JavaScript, React 이벤트엔 hover가 없다.
따라서, JavaScript, React에 hover 이벤트(편의상 hover 이벤트라고 부르겠다.)를 적용하고 싶다면 마우스 커서가 해당 요소에 위치O, 위치X 모두 이벤트를 적용해주면 된다.

이번 구현 과정에선 MouseEnter, MouseLeave event를 활용하였다.
그리고 단위가 0.5라서 반 별 SVG를 활용하였다.


SVG 파일 컴포넌트로 사용하기

  • SVG를 ReactComponent로 export하여 사용
  • SVG는 Font Awesome에서 copy하여 사용
export { ReactComponent as SolidStarHalfIcon } from './solid-star-half.svg';

JSX

JSX 구조는 다음과 같이 작성하였다.
5개의 별이 들어갈 container(div) > button > SVG 로 구성하였다.

  <div className={cx(styles.starRateContainer, className)}>
      {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((num) => (
        <button 
          key={num}
          className={cx(styles.wrapper, num % 2 || styles.right)}
        >
          <SolidStarHalfIcon 
            className={styles.star}
          />
        </button>
      ))}
  </div>

button으로 한번 더 감싼 이유는, SVG 파일의 크기가 실제 눈으로 보이는 크기의 2배였기 때문에 button으로 먼저 크기 제한을 하고, overflow 되는 부분을 숨기기 위해서였다.

처음엔 이 구조가 아닌 Container(div) > SVG 구조로 구현하였는데, 이후에 기능 구현을 하였더니 마우스를 반쪽 별에만 올려도 별 하나씩 색칠되는 현상이 나타났다.

→ 이는 svg의 크기가 실제로 보이는 반쪽이 아닌, 별 하나만큼의 크기를 가지기 때문이었다. (검정색 border로 표시된 공간만큼의 크기를 가지고 있다.)


CSS

  • 핵심은 반 별 하나가 가지는 빈 공간을 제거하고 별이 하나처럼 보이도록 만드는 것
  • 설명은 주석으로 대체
  • stack: SCSS, classnames 사용
@use "../../../styles/mixins" as m;

// display: flex, 가로세로 center
.starRateContainer {
  @include m.flex();
  max-width: 160px;
  max-height: 32px;
}

// button을 의미
.wrapper { 
  height: 32px;
  width: 16px; // 크기를 SVG의 반으로 적용
  background: none;
  border: none;
  overflow: hidden; // SVG 빈 공간 숨기기

  // 반 별 SVG
  > .star { 
    height: 32px;
    width: 32px;
    transition: transform 0.2s ease-in;
  }

 // & 선택자로 button 선택, index 홀수면 .right className 추가
 // 오른쪽 반 별 SVG 뒤집기
  &.right { 
    transform: rotateY(-180deg);
  }
}

Hover event (MouseEnter, MouseLeave)

  • onMouseEnter, onMouseLeave를 통해 hover 이벤트 적용
  • event 발생 시 실행되는 callback 함수 fillStarOfIndex를 통해 SVG fill 속성 변경
  const [hoveredStarIndex, setHoveredStarIndex] = useState(0);
  const [clickedStarIndex, setClickedStarIndex] = useState(value);

// num은 별SVG의 index
  const fillStarOfIndex = (num, event) => {
    if (event === 'enter' && hoveredStarIndex >= num) {
      return '#ff9900'; // grey
    }
    if (event === 'leave' && clickedStarIndex >= num) {
      return '#ff9900'; // grey
    }
    return '#eeeeee'; // yellow
  };


// onChange는 상위 컴포넌트의 객체 state를 업데이트하는 props
// 별SVG의 click 이벤트 발생 시 업데이트
  const onClickStar = (num) => {
    return () => {
      setClickedStarIndex(num);
      onChange((prev) => {
        return { ...prev, ['score']: num / 2 };
      });
    };
  };
...
<button
  ...
   onMouseEnter={() => setHoveredStarIndex(num)}
   onMouseLeave={() => setHoveredStarIndex(0)}
   onClick={onClickStar(num)}
>
    <SolidStarHalfIcon
      ...
      fill={fillStarOfIndex(
        num,
        hoveredStarIndex === 0 ? 'leave' : 'enter',
      )}
     />
</button>

...
profile
기록 이사중 🐣

0개의 댓글