React

[React] 별점 기능 구현

hid1 2022. 12. 14. 17:19

별점 기능에 대해 참고할만한 글이나 영상들을 찾아보다가 

리액트로 별 반개 씩 별점을 주는 기능은 별로 없어서 써본다.

 

import { useState } from "react";
import StarInput from "./StarInput";

const StarRating = () => {
  const [rating, setRating] = useState(0);

  const handleClickRating = (value) => {
    setRating(value);
  };

  return (
    <section>
      <h1>별점</h1>
      <fieldset>
         <StarInput
          onClickRating={handleClickRating}
          value={5}
          isHalf={false}
        />
        // 생략
        <StarInput
          onClickRating={handleClickRating}
          value={0.5}
          isHalf={true}
        />
      </fieldset>
      <span>{rating}</span>
    </section>
  );
};

export default StarRating;

대략 전체적인 구조로 useState를 사용하여 rating이란 이름으로 상태를 관리하였고

라디오 인풋으로 5점부터 ~ 0.5점까지 0.5 단위 씩 별점을 value 값으로 주어 StarInput 컴포넌트를 만들었다.

isHalf props은 별점이 지금 0.5 단위인지 진리값으로 주었다. 값이 true이면 별의 반개인 아이콘이 나타난다.

import { FaStar, FaStarHalf } from "react-icons/fa";

const StarInput = ({ onClickRating, value, isHalf }) => {
  const handleClickRatingInput = (value) => {
    onClickRating(value);
  };

  return (
    <>
      <input type="radio" name="rating" id={`star${value}`} value={value} />
      <label
        onClick={handleClickRatingInput}
        isHalf={isHalf}
        htmlFor={`star${value}`}
      >
        {isHalf ? <FaStarHalf /> : <FaStar />}
      </label>
    </>
  );
};

export default StarInput;

 

css로 스타일을 어떻게 구현해야할지 가장 고민이었다.

지금은 별 반별 별 반별 별 반별 별 반별 별 반별 이렇게 나열되고 있지만

(반별 별) (반별 별) (반별 별) (반별 별) (반별 별)과 같이 요소가 겹쳐지게 해야했다.

 

이에 대해 바닐라 자바스크립트로는 fontawesome으로 ::after 요소에 반개의 별 아이콘 유니코드를 넣어 position: absolute로 위치를 잡아서 구현한 경우도 많이 보였다. 하지만 한 번 시도해보았으나 지원 하지 않는 건지 되지 않았고 원하는 아이콘이 아니었기 때문에 이 방법으로는  사용하지 않았다.

 

생각을 여러 번 해도 좋은 해결책이 나오지 않아서 어쩔 수 없이 transform을 이용하여 조정을 하였다.

half의 상태가 true인 컴포넌트에 position: absolute와 transform을 이용해 width에 맞게 조정을 했다.

 

전체코드

import styled from "@emotion/styled";
import { useState } from "react";
import StarInput from "./StarInput";

const Base = styled.section`
  display: flex;
  align-items: center;
  gap: 8px;
`;

const Name = styled.span`
  font-size: 1.4rem;
  line-height: 100%;
`;

const RatingValue = styled.span`
  font-size: 1.2rem;
  line-height: 100%;
`;

const RatingField = styled.fieldset`
  position: relative;
  display: flex;
  align-items: center;
  flex-direction: row-reverse;
  border: none;
  transform: translateY(2px);

  input:checked ~ label,
  labeL:hover,
  labeL:hover ~ label {
    transition: 0.2s;
    color: orange;
  }
`;

const StarRating = () => {
  const [rating, setRating] = useState(0);

  const handleClickRating = (value) => {
    setRating(value);
  };

  return (
    <Base>
      <Name>별점</Name>
      <RatingField>
        <StarInput
          onClickRating={handleClickRating}
          value={5}
          isHalf={false}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={4.5}
          isHalf={true}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={4}
          isHalf={false}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={3.5}
          isHalf={true}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={3}
          isHalf={false}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={2.5}
          isHalf={true}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={2}
          isHalf={false}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={1.5}
          isHalf={true}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={1}
          isHalf={false}
        />
        <StarInput
          onClickRating={handleClickRating}
          value={0.5}
          isHalf={true}
        />
      </RatingField>
      <RatingValue>{rating}</RatingValue>
    </Base>
  );
};

export default StarRating;
import styled from "@emotion/styled";
import { css } from "@emotion/react";
import { FaStar, FaStarHalf } from "react-icons/fa";

const Input = styled.input`
  display: none;
`;

const Label = styled.label`
  cursor: pointer;
  font-size: 1.5rem;
  color: lightgray;

  ${({ isHalf }) =>
    isHalf &&
    css`
      position: absolute;
      width: 12px;
      overflow: hidden;

      &:nth-of-type(10) {
        transform: translate(-108px);
      }
      &:nth-of-type(8) {
        transform: translate(-84px);
      }
      &:nth-of-type(6) {
        transform: translate(-60px);
      }
      &:nth-of-type(4) {
        transform: translate(-36px);
      }
      &:nth-of-type(2) {
        transform: translate(-12px);
      }
    `}
`;

const StarInput = ({ onClickRating, value, isHalf }) => {
  const handleClickRatingInput = (value) => {
    onClickRating(value);
  };

  return (
    <>
      <Input type="radio" name="rating" id={`star${value}`} value={value} />
      <Label
        onClick={handleClickRatingInput}
        isHalf={isHalf}
        htmlFor={`star${value}`}
      >
        {isHalf ? <FaStarHalf /> : <FaStar />}
      </Label>
    </>
  );
};

export default StarInput;

 

 

 

StarInput 컴포넌트에서 별점을 5부터 시작하는 이유가 있다.

별에 마우스를 대거나 클릭을 할 경우, 만약 3점을 클릭하면 나머지 0.5~2.5 선택이 모두 되어야 한다.

~ 선택자는 요소의 다음 형제 요소들을 선택하기 때문에 순서를 반대로 하였다.

그리고 flex-direction:row-reverse;로 모습이 반대로 바뀌게 하였다.

 

const RatingField = styled.fieldset`
  position: relative;
  display: flex;
  align-items: center;
  flex-direction: row-reverse;

  input:checked ~ label,
  labeL:hover,
  labeL:hover ~ label {
    transition: 0.2s;
    color: orange;
  }
`;

 

 

 

구현된 모습

 

 

 

반응형