별점 기능 구현하기

스탁벅스·2023년 5월 19일
1
post-thumbnail


별점 기능을 구현해봤다. 단순히 별을 클릭해서 별점을 매기는 것이 아니라, 왓챠피디아 처럼 마우스가 오버되는 위치에 따라 별이 채워지고, 클릭했을 때 해당 별점이 고정되게 구현했다. 또한 0.5점 부터 5점까지, .5점 단위로 별점을 매길 수 있게 하였는데 이 부분이 굉장히 까다로웠다. 마우스가 별의 오른쪽 절반에 있을 때는 별 전체가 채워지고, 왼쪽 절반에 위치해 있을 때 별의 왼쪽 절반만 채워지도록 해야하기 때문에 고민을 많이 했다.
챗지피티한테도 물어보고 여러 블로그 글들을 읽어봤지만 시원하게 해결이 안돼서 직접 아이디어를 내서 구현했다.

1. 별

우선 별에 딱 맞는 크기의 div(StarDiv)를 만든다. 그리고 이 div를 가로로 정확히 절반을 나누어 두 개의 div로 쪼갠다(Left, Right). 그리고 이제 StarDiv의 position을 relative로 설정하고 별의 position을 absolute로 설정한 뒤 별을 StarDiv 안에 넣는다.

이제 마우스가 왼쪽 div(Left)에 위치한지 , 오른쪽 div(Right)에 위치한지에 따라 별을 다르게 채우면된다.

2. 마우스가 별 위에 올라갈 때

다섯개의 별들 중에서 몇번째 별에 마우스가 올라가 있는지에 따라 점수가 결정된다. 예를 들어 3번째 별의 왼쪽 절반에 마우스가 올라가 있으면 2.5점, 오른쪽 절반에 마우스가 올라가 있으면 3점이다.


...

const [score, setScore] = useState<number>(0);

const handleLeftHalfEnter = (idx: number) => setScore(idx + 0.5);

const handleRightHalfEnter = (idx: number) => setScore(idx + 1);

 const handleStarLeave = (idx: number) => {
  };

...
	return(
       <RowBox>
 		{Array(5)
        .fill(0)
        .map((i, idx) => (
          <StarDiv key={idx}>
            {score - Math.floor(score) === 0.5 && Math.floor(score) === idx ? ( //1. 절반만 채워진 별
              <FaStarHalf 
                key={idx}
                style={{ position: "absolute" }}
                size={32}
                color="gold"
              />
            ) : idx + 1 > score ? ( //2. 빈 별
              <FaStar
                key={idx}
                style={{ position: "absolute" }}
                size={32}
                color="lightGray"
              />
            ) : (  //3. 완전히 채워진 별
              <FaStar
                key={idx}
                style={{ position: "absolute" }}
                size={32}
                color="gold"
              />
            )}
              <Left
              key={idx + "left"}
              onMouseEnter={() => handleLeftHalfEnter(idx)}
              onMouseLeave={() => handleStarLeave(idx)}
            />
            <Right
              key={idx + "right"}
              onMouseEnter={() => handleRightHalfEnter(idx)}
              onMouseLeave={() => handleStarLeave(idx)}
            />
          </StarDiv>
        ))}
      </RowBox>  
      );

크기가 5인 배열로 map을 활용해서 5개의 별을 그린다. 이때 점수(score)에 따라서 나타날 수 있는 별이 세가지로 나뉜다.

1. 절반만 채워진 별
2. 빈 별
3. 완전히 채워진 별

예를 들어 마우스가 세번째 별의 왼쪽 절반 위에 있을 때, handleLeftHalfEnter 함수가 세번째 별의 인덱스(=2)가 파라미터로 들어가서 실행되고, score state가 2.5가 된다.
이때, 인덱스가 2인 별은 절반만 채워진 별이되고, 인덱스+1가 2.5보다 큰 별은 빈별이 되고, 나머지는 완전히 채워진 별이된다.
이런식으로 마우스의 위치에 따라 score값이 바뀌고, 바뀐 score값에 따라 어떤 별이 보여질지 결정된다.

3. 별 클릭시

위에 주어진 왓챠피디아 예시에서는, 별을 클릭해야 완전히 점수가 바뀐다. 예를 들어 초기 별점이 2점인 상태에서, 다섯번째 별의 오른쪽 절반에 마우스를 올리면 별 5개가 모두 채워진다. 하지만 마우스를 다시 별 밖으로 위치시키면, 다시 별이 이전 점수에 맞게, 별점 2점인 상태로 돌아간다. 별점 5점으로 완전히 바꾸려면 마우스를 다섯번째 별의 오른쪽 절반에 올린상태에서, 클릭까지 해야한다.

이를 구현하기 위해 scoreFixed라는 state를 추가로 선언하고, 마우스를 클릭했을 때 이 scoreFixed값을 현재 score값과 같은 값으로 바꿔준다. 또한 마우스가 별 밖으로 벗어날 때, score값과 scoreFixed값이 다르다면, score값을 scoreFixed값과 같은 값으로 바꿔준다. 둘의 값이 다르다는 것은 클릭하지 않았다는 뜻이기 때문에, 마우스가 별 밖으로 벗어나면 이전 점수인 scoreFixed 값으로 score를 되돌리는 것이다.

4. 최종 코드

(별 아이콘은 react-icons에서 가져왔다)

function StarRating() {
  const [score, setScore] = useState<number>(0);
  const [scoreFixed, setScoreFixed] = useState(score);

  const handleLeftHalfEnter = (idx: number) => setScore(idx + 0.5);

  const handleRightHalfEnter = (idx: number) => setScore(idx + 1);

  const handleStarClick = () => {
    setScoreFixed(score);
  };

  const handleStarLeave = () => {
    if (score !== scoreFixed) {
      setScore(scoreFixed);
    }
  };

  return (
    <RowBox>
      {Array(5)
        .fill(0)
        .map((i, idx) => (
          <StarDiv key={idx} onClick={handleStarClick}>
            {score - Math.floor(score) === 0.5 && Math.floor(score) === idx ? (
              <FaStarHalfAlt
                key={idx}
                style={{ position: "absolute" }}
                size={32}
                color="gold"
              />
            ) : idx + 1 > score ? (
              <FaStar
                key={idx}
                style={{ position: "absolute" }}
                size={32}
                color="lightGray"
              />
            ) : (
              <FaStar
                key={idx}
                style={{ position: "absolute" }}
                size={32}
                color="gold"
              />
            )}
            <Left
              key={idx + "left"}
              onMouseEnter={() => handleLeftHalfEnter(idx)}
              onMouseLeave={handleStarLeave}
            />
            <Right
              key={idx + "right"}
              onMouseEnter={() => handleRightHalfEnter(idx)}
              onMouseLeave={handleStarLeave}
            />
          </StarDiv>
        ))}
    </RowBox>
  );
}

5. 결과

왓챠피디아와 똑같이 작동한다!
가장 효율적인 방법인지는 확실하지 않지만, 오랫동안 고민해서 생각한대로 작동하는 결과를 얻어서 뿌듯하다.

profile
환영합니다. 스탁벅스입니다.

0개의 댓글