Basic_Studies/React & Next.js

[React] svg로 비율 정확한 별점 구현

adore_voy 2022. 7. 13. 14:06

오늘 실습해 볼 것은 아래와 같습니다.

주어진 데이터는 100점 만점 기준 해당 브랜드의 평균 평점입니다. 아마 93점 이렇게 나오겠죠?
별점은 정말 세세하게 퍼센트로 채워짐을 표현해야합니다.

 

물론! 이를 구현함에 있어 정말 많은 좋은 방법들이 있습니다.

처음 떠올렸던 방법은 별모양 다섯개를 한 묶음으로 status bar를 제작했던 원리와 비슷하게 overflow hidden으로 채워진 것 과 같은 표현을 주는 것 이었습니다.

 

그러나 주어진 별모양 파일은 svg였고 새로 만들고 싶지 않았습니다. 전체 웹사이트에 통일된 코드기도 했고요.

때문에 svg마다의 clip-path 지정해서 5개로 길이를 나누는 방법을 사용하려고 합니다.


별 하나만 있는 샘플 코드는 아래와 같습니다.

보라색을 좋아해서 보라색으로 세팅했습니다.

Styled-components 사용할것이지만 어차피 css 파일 따로 만드는 말곤 차이가 거의 없으니 입맛대로 설정해주시면 됩니다.

import styled from "styled-components";

function StarRate() {
    return (
        <StarRateWrap>
            <span className='star_icon'>
                <svg xmlns='http://www.w3.org/2000/svg' width='40' height='39' viewBox='0 0 14 13'>
                    <path
                        id='star'
                        d='M9,2l2.163,4.279L16,6.969,12.5,10.3l.826,4.7L9,12.779,4.674,15,5.5,10.3,2,6.969l4.837-.69Z'
                        transform='translate(-2 -2)'
                        fill='#966fd6'
                    />
                </svg>
            </span>
        </StarRateWrap>
    )
}

export default StarRate;

const StarRateWrap = styled.div`
        display: flex;
        align-items: center;
        width: 100%;
        margin: 100px 0 0 15px;
        .star_icon {
          display: inline-flex;
          margin-right: 5px;
        }
      
`

위 컴포넌트는 아래와 같은 모양의 별을 그립니다.

다섯개를 만들어볼까요?

span 부분을 다섯개 복사하면 됩니다.

지저분해보이기 때문에 이후에 맵으로 돌릴겁니다.

이제 clip-path를 이용해서 절반만 채워진 별을 그려 볼 건데요

아래와 같이 사용합니다.

function StarRate() {
    return (
        <StarRateWrap>
            <span className='star_icon'>
                {/* 원래 별 모양은 svg로 잡습니다. 때문에, svg 컬러는 '비어있을 때' 별의 색상을 설정합니다. */}
                <svg xmlns='http://www.w3.org/2000/svg' width='40' height='39' viewBox='0 0 14 13' fill='#cacaca'>
                    {/* clip-path는 임의의 사각형을 만들어 별 위에 덮어주기로 했습니다. */}
                    <clipPath id="firstStarClip">
                        {/* 여기서 width는 svg의 viewBox 기준입니다. width가 14이기 때문에 절반만 채워주고 싶다면 7이 되는 것 입니다. */}
                        <rect width='7' height='39' />
                    </clipPath>
                    <path
                        id='firstStar'
                        d='M9,2l2.163,4.279L16,6.969,12.5,10.3l.826,4.7L9,12.779,4.674,15,5.5,10.3,2,6.969l4.837-.69Z'
                        transform='translate(-2 -2)'
                    />
                    {/* clip-path를 사용하기 위한 태그입니다. 채워졌을 때 색상을 설정하면 되고, 각 id를 잘 맞춰 적어주어야 합니다.  */}
                    <use clipPath="url(#firstStarClip)" href="#firstStar" fill='#966fd6'
                    />
                </svg>
            </span>
        </StarRateWrap>
    )
}

그럼 아래와 같이 나옵니다.

이제 좀 느낌이 오시나요?
우리가 계산해야 할 값은 rect의 width입니다. max값은 viewBox width의 max인 14이고요.

동적으로 계산해주어야 하기 때문에 아래와 같이 설정해봅시다.

const AVR_RATE = 80; // 상품 평균 평점. 실제로는 데이터에서 패치할 것 입니다.
    const STAR_IDX_ARR = ['first', 'second', 'third', 'fourth', 'last']; // 다섯개의 별을 따로 컨트롤하기 위해서는 고유 id를 각각 가지고 있어야 합니다. 이 고유 아이디를 쉽게 생성해 주기 위한 리스트 입니다.
    const [ratesResArr, setRatesResArr] = useState([0, 0, 0, 0, 0]);    // 별점 리스트 상태입니다.
    const calcStarRates = () => {
        let tempStarRatesArr = [0, 0, 0, 0, 0]; // 임시 리스트.
        let starVerScore = (AVR_RATE * 70) / 100;   // 별 한 개 당 width가 14이므로 총 70. 100점 만점인 현재와 비율을 맞춰줍니다.
        let idx = 0;
        while (starVerScore > 14) { // 14를 starVerScore에서 하나씩 빼가면서 별 하나하나에 채워질 width를 지정해줍니다. 다 채워지지 않을 인덱스의 별은 아래 tempStarRatesArr[idx] = starVerScore; 에서 채워줍니다.
          tempStarRatesArr[idx] = 14;
          idx += 1; // 인덱스 0부터 첫번째 별 입니다.
          starVerScore -= 14;
        }
        tempStarRatesArr[idx] = starVerScore;
        return tempStarRatesArr;    // 평균이 80이라면 [14, 14, 14, 14, 0] 이 되겠죠?
      };
    useEffect(() => {
        setRatesResArr(calcStarRates)   // 별점 리스트는 첫 렌더링 때 한번만 상태를 설정해줍니다.
    }, [])

이제 별을 결과 값과 함께 맵으로 돌려보겠습니다.

return (
        <StarRateWrap>
            {STAR_IDX_ARR.map((item, idx) => {
                return <span className='star_icon' key={`${item}_${idx}`}>
                    <svg xmlns='http://www.w3.org/2000/svg' width='40' height='39' viewBox='0 0 14 13' fill='#cacaca'>
			{/* id는 별 하나하나 마다 다른 값을 가지고 있어야 합니다 */}
                        <clipPath id={`${item}StarClip`}>
                            {/* 새로 생성한 리스트에서 별 길이를 넣어줍니다. */}
                            <rect width={`${ratesResArr[idx]}`} height='39' />
                        </clipPath>
                        <path
                            id={`${item}Star`}
                            d='M9,2l2.163,4.279L16,6.969,12.5,10.3l.826,4.7L9,12.779,4.674,15,5.5,10.3,2,6.969l4.837-.69Z'
                            transform='translate(-2 -2)'
                        />
                        <use clipPath={`url(#${item}StarClip)`} href={`#${item}Star`} fill='#966fd6'
                        />
                    </svg>
                </span>
            })
            }
        </StarRateWrap>
    )

결론적으로 아래와 같은 평균 80점의 별이 나오게 됩니다.

사실 AVR_RATE 변수의 숫자만 바꿔주면 얼마든지 아래와 같은 73점의 별점도 나타낼 있죠.


모듈을 만들거나 함수나 커스텀 훅을 만들어 사용해도 편리할 것 같습니다.

최대한 자세히 주석을 달아봤는데 도움이 됐으면 좋겠습니다.

아래는 최종 코드입니다.

import styled from "styled-components";
import { useState, useEffect } from "react";

function StarRate() {
    const AVR_RATE = 73;
    const STAR_IDX_ARR = ['first', 'second', 'third', 'fourth', 'last'];
    const [ratesResArr, setRatesResArr] = useState([0, 0, 0, 0, 0]);
    const calcStarRates = () => {
        let tempStarRatesArr = [0, 0, 0, 0, 0];
        let starVerScore = (AVR_RATE * 70) / 100;
        let idx = 0;
        while (starVerScore > 14) {
            tempStarRatesArr[idx] = 14;
            idx += 1;
            starVerScore -= 14;
        }
        tempStarRatesArr[idx] = starVerScore;
        return tempStarRatesArr;
    };
    useEffect(() => {
        setRatesResArr(calcStarRates)
    }, [])
    return (
        <StarRateWrap>
            {STAR_IDX_ARR.map((item, idx) => {
                return <span className='star_icon' key={`${item}_${idx}`}>
                    <svg xmlns='http://www.w3.org/2000/svg' width='40' height='39' viewBox='0 0 14 13' fill='#cacaca'>
                        <clipPath id={`${item}StarClip`}>
                            <rect width={`${ratesResArr[idx]}`} height='39' />
                        </clipPath>
                        <path
                            id={`${item}Star`}
                            d='M9,2l2.163,4.279L16,6.969,12.5,10.3l.826,4.7L9,12.779,4.674,15,5.5,10.3,2,6.969l4.837-.69Z'
                            transform='translate(-2 -2)'
                        />
                        <use clipPath={`url(#${item}StarClip)`} href={`#${item}Star`} fill='#966fd6'
                        />
                    </svg>
                </span>
            })
            }
        </StarRateWrap>
    )
}

export default StarRate;

const StarRateWrap = styled.div`
        display: flex;
        align-items: center;
        width: 100%;
        margin: 100px 0 0 15px;
        .star_icon {
          display: inline-flex;
          margin-right: 5px;
        }
      
`
반응형