[CSS 고급] mix-blend-mode CSS 속성으로 소수점 단위 별점 구현하기

mix-blend-mode 속성은 생소하지만, 모던 웹 브라우저에서는 모두 지원하는 안정화된 속성입니다.

이 속성은 겹쳐진 두 개의 레이어의 색상이 어떻게 섞이는지를 결정하는 속성입니다.

두 레이어의 색상을 혼합해서 하나의 레이어로 합쳐져 보이도록 만드는 컬러 필터 이펙트 효과입니다.

효과는 극적이지만, 활용할 수 있는 용도가 그렇게 많지는 않습니다.

별점 기능은 만드는 방법이 여러 가지 있고, 자바스크립트의 도움을 받으면 소수점 단위 별점 기능을 어렵지 않게 구현할 수도 있습니다.

mix-blend-mode 속성을 이용하면 기존의 구현 방법보다  더 간결하고 빠르게 소수점 별점 기능을 구현할 수 있습니다.

mix-blend-mode 속성은 배경색과 전경 색의 색상을 혼합해서 색상을 얻는 컬러 필터 기능을 하는 속성입니다.

속성 값은 16가지나 있기 때문에 자세한 내용은 다음 링크에서 확인하면 됩니다.

> mix-blend-mode

별점 기능을 구현하기 위해 우리가 사용하는 속성 값은 "color"입니다.

배경색의 채도를 전경 색의 명도만큼 낮추는 속성 값으로 쉽게 말해 전경 색이 검정이면 배경색 색상이 완전한 회색톤으로 보이게 됩니다.

아래 그림에서처럼 별점 이미지 일우 영역 위에 위치한 오버레이 마스크 레이어 적용한 검은색과 mix-blend-mode 속성은 배경의 노란색 별점을 회색으로 보이게 만듭니다. 이 효과를 이용해 마스크 너비를 늘리고 줄이면서 별점의 별 일부만 채워져 있는 효과를 만들어낼 수 있습니다.

HTML 태그와 CSS를 만들면서 위 그림을 참고하면 도움이 됩니다.

완성된 코드는 다음 링크를 클릭해 다운로드할 수 있습니다.

starrating.zip0.00MB


1. SVG 포맷 별 이미지 준비

먼저 SVG 포맷인 별 이미지 하나를 준비합니다.

오픈소스로 배포되는 별 이미지가 많이 있으므로 검색해서 사용하면 됩니다.

폰트어썸 별 이미지

star.zip0.00MB

꼭 SVG 이미지여야 하는 것은 아니지만, 구현의 편의성이나 CSS로 별의 색상을 직접 조절할 수 있는 장점이 있으므로 SVG 포맷 이미지를 사용하는 것이 좋습니다.

CSS로 별 이미지의 색상을 변경할 수 있도록 하기 위해서는 SVG 이미지를 인라인 SVG로 CSS 안에 저장해야 합니다.

그래야 CSS 클래스를 SVG 이미지에 적용할 수 있습니다.

SVG 이미지를 텍스트 편집기에서 열면 다음과 같이 XML 포맷으로 된 텍스트 파일 내용이 표시됩니다.(위의 샘플 별 이미지의 XML 소스)

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M381.2 150.3L524.9 171.5C536.8 173.2 546.8 181.6 550.6 193.1C554.4 204.7 551.3 217.3 542.7 225.9L438.5 328.1L463.1 474.7C465.1 486.7 460.2 498.9 450.2 506C440.3 513.1 427.2 514 416.5 508.3L288.1 439.8L159.8 508.3C149 514 135.9 513.1 126 506C116.1 498.9 111.1 486.7 113.2 474.7L137.8 328.1L33.58 225.9C24.97 217.3 21.91 204.7 25.69 193.1C29.46 181.6 39.43 173.2 51.42 171.5L195 150.3L259.4 17.97C264.7 6.954 275.9-.0391 288.1-.0391C300.4-.0391 311.6 6.954 316.9 17.97L381.2 150.3z"/></svg>

그중 <path> 태그만 인라인 SVG로 가져오게 됩니다. 대부분의 SVG 별 이미지는 한 개의 패스로 만든 닫힌 경로로 만들어져 있으므로 1개의 <path> 태그만 있습니다.

경로 데이터, 주석문, 불필요한 속성을 제외한 별 SVG 이미지의 구조는 다음과 같습니다.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="패스데이터..."/></svg>

여기에 별 색상 제어를  CSS로 하기 위해서 속성을 추가해야 합니다.

<path> 태그에 속성 2개(fill="none" class="starcolor")를 추가합니다. fill="none" 속성은 패스의 내부를 기본 컬러로 채우지 않는 속성입니다. 중요합니다. 이 속성이 없으면 CSS로 정한 색상이 적용되지 않습니다.

이제 CSS에 ".starcolor" 클래스를 정의해서 SVG 별 이미지의 채우기 색상을 제어할 수 있습니다.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="none" class="starcolor" d="패스데이터..."/></svg>

여기까지 하면 SVG 별 이미지 준비는 끝났습니다.

CSS로 색상을 적용할 때는 일반 CSS 요소에 색상을 적용하는 방법과 다른 방법으로 적용해야 합니다.

SVG 이미지 객체(태그)에 채우기 색상을 적용할 때는 "fill" 속성으로 색상을 적용합니다.

SVG 이미지 전용으로 사용할 수 있는 속성이므로 주의해야 합니다. 

CSS로 ".starcolor" 클래스에 채우기 색상을 적용할 때는 다음과 같이 적용합니다.

.starcolor{
    fill: #ff8844;
}

이제 이 SVG 별 이미지를 자바스크립트를 사용해서 사용할 개수만큼 DOM에 붙여 넣으면 됩니다.

미리 SVG 이미지를 붙여 넣는 코드를 살짝 보여주면 다음과 같습니다. 실제 구현 코드는 아래에서 다룹니다.

let el = document.createElement('div');
//인라인 SVG 이미지 부착
el.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="none" class="starcolor" d="경로데이터..."/></svg>';

번거로운 과정을 거치게 되지만, 벡터 데이터에 CSS 클래스를 적용해서 다양한 CSS 속성을 부여할 수 있는 자유도가 부여되므로 이 방법으로 구현해보는 것을 추천합니다.


2. HTML 준비

HTML에는 특별히 주의해야 할 부분은 없습니다.

알아둘 중요 태그는 다음과 같습니다.

1. 별점을 숫자로 표시하는 입력 필드 1개.

2. SVG 이미지는 자바스크립트를 이용해 동적으로 붙여 넣을 것이기 때문에 별도로 HTML 태그로 표현하는 내용은 없고, 별 SVG 이미지들을 담아서 감싸기 위한 ".rating" 클래스를 가진 태그 하나, 그리고 외곽에 별점 테두리에 여백을 설정하기 위한 ".rating-wrap" 클래스를 가진 태그 하나가 필요합니다.

3. ".overlay" 클래스 태그는 별 이미지들 영역 일부를 마스킹해서 별점 표시를 하는 중요 태그입니다. 실질적으로 이 마스킹 태그가 mix-blend-mode 속성을 이용하는 별점 구현의 핵심입니다.

<div class="container">
  <label>
    Rate Value <input type="number" name="ratevalue" value="3" step="0.1" min="0" max="5" />
  </label>
  <div class="rating-wrap">
    <div class="rating">
        <div class="overlay"></div>
    </div>
  </div>
</div>

3. CSS로 기본 레이아웃 만들기

별 이미지의 크기는 자바스크립트에서 "starsize" 상수 값으로 제어하기 때문에 CSS에서 별도의 크기 설정은 하지 않습니다. 별 이미지들을 가로로 배치하는 디자인 레이아웃만 적용하면 됩니다.

별 이미지 사이에 여백을 띄우는 경우에 대한 처리는 자바스크립트로 구현하므로 별도의 제한 없이 별 사이 여백을 설정하면 됩니다.

"starcolor" 클래스는 인라인 SVG 별 이미지에 색상을 정하는 클래스입니다.

알아둘 중요 내용은 다음과 같습니다.

1. body 태그, 또는 별 SVG 이미지를 감싸는 외곽 태그 중 하나에는 반드시 배경색이 있어야 합니다.

배경색을 흰색으로 사용하는 경우에도 반드시 흰색으로 명시적으로 배경색(background-color)을 부여해야 합니다.

mix-blend-mode 속성은 배경색이 없을 경우 블랜딩 효과가 적용되지 않고 전경 색이 그대로 표현됩니다.

별점 표현을 위해 별 이미지들의 일부를 가지면서 가려진 별 이미지들의 색상이 블랜딩 되는 것과 별 이미지 외곽은 투명 처리가 되어야 하는데 이 부분에 전경 색이 그대로 노출됩니다.

주의해야 합니다.

2. ".rating-wrap" 클래스는 별점 이미지들을 감싸는 클래스인 ".rating" 클래스를 감싸서 여백을 추가하는 용도로만 사용합니다.

".rating" 클래스는 별 이미지들을 감싸는 역할을 하는데, 이 클래스에 여백을 주면 마스크 너비를 계산할 때 여백까지 고려해야 하기 때문에 계산이 복잡해집니다. 그래서 자바스크립트로 마스크 너비를 계산할 때 여백을 따로 고려하지 않아도 되도록 감싸는 태그를 하나 더 추가한 것입니다.

3. ".starcolor" 클래스는 인라인 SVG 이미지에 적용하는 별 이미지 컬러 속성용 클래스입니다.

4. ".overlay" 클래스는 별 이미지들 위에 위치하는 마스킹 클래스입니다. 별점 구현의 핵심이며, 이 클래스에 mix-blend-mode 속성을 정의해서 배경 레이어의 색상을 블랜딩 해서 별점 부여 효과를 만듭니다.

예제는 호환성 문제를 피하기 위해 @supports로 mix-blend-mode를 정의했는데, 대부분의 모던 웹 브라우저가 mix-blend-mode 속성을 지원하기 때문에 굳이 이렇게 호환성 코드를 추가할 필요까지는 없습니다.

메인 ".overlay" 클래스에 직접 mix-blend-mode 속성을 부여해도 됩니다.

body{
    background-color: #f0f0f0;
}
input[type=number]{
    margin-bottom: 0;
    margin-left: 8px;
    padding: 6px 8px;
    font-size: 1em;
    border: none;
    border-radius: 4px;
}
.rating-wrap{
    padding: 10px;
    display: flex;
}
.rating {
    display: flex;
    align-items: center;
    position: relative;
}
.starcolor{
    fill: #ff8844;
}
.star:last-of-type {
    margin-right: 0;
}
.overlay {
    background-color: #000;
    opacity: 0.5;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 1;
    transition: all 0.3s ease-in-out;
}
@supports (mix-blend-mode: color) {
    .overlay{
        mix-blend-mode: color;
        opacity: unset;
    }
}

예제에서는 자바스크립트에서 별 이미지의 크기와 여백 값을 변수로 관리하지만, 고정 크기로 CSS에서 크기를 설정하고 싶으면 다음처럼 개별 별 이미지를 감싸는 <div> 태그에 ".star" 클래스를 추가해도 됩니다.

.star {
    width: 30px;
    height: 30px;
    margin-right: 2px;
}

4. 별점 계산을 위한 초기화 및 이벤트 리스너 생성

예제에서는 사용자가 별점을 선택했다는 가정하에 최종 입력된 값을 <input> 태그로 받아서 소수점 별점을 부여합니다.

입력값이 바뀌는 데 따라 별점이 자동으로 적용되도록 이벤트 리스너를 등록해서 구현합니다.

그리고, 반대로 마우스로 별점 영역을 클릭하면 클릭한 위치만큼의 별점이 부여되고, 입력 필드에도 해당 값이 소수점 첫 째 자리까지 표현되도록 하는 마우스 이벤트 리스너도 구현을 합니다.

별점을 계산해서 적용하는 로직을 제외한 이벤트를 처리하는 소스는 다음과 같습니다.

const starSize = 30, maxStar = 5, gutter = 2;//별 크기, 별 개수
let maskMax = 0; //오버레이 마스크 최대 너비

window.addEventListener('DOMContentLoaded',()=>{
    //별 이미지 SVG 개수만큼 생성
    for(let i = 0;i < maxStar;i++){
        let el = document.createElement('div');
        el.style.width = starSize + 'px';
        el.style.height = starSize + 'px';
        el.style.marginRight = gutter + 'px';
        //인라인 SVG 이미지 부착
        el.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="none" class="starcolor" d="M381.2 150.3L524.9 171.5C536.8 173.2 546.8 181.6 550.6 193.1C554.4 204.7 551.3 217.3 542.7 225.9L438.5 328.1L463.1 474.7C465.1 486.7 460.2 498.9 450.2 506C440.3 513.1 427.2 514 416.5 508.3L288.1 439.8L159.8 508.3C149 514 135.9 513.1 126 506C116.1 498.9 111.1 486.7 113.2 474.7L137.8 328.1L33.58 225.9C24.97 217.3 21.91 204.7 25.69 193.1C29.46 181.6 39.43 173.2 51.42 171.5L195 150.3L259.4 17.97C264.7 6.954 275.9-.0391 288.1-.0391C300.4-.0391 311.6 6.954 316.9 17.97L381.2 150.3z"/></svg>';
        document.querySelector('.rating').appendChild(el);//입력 필드 최대값 재설정
    }

    maskMax = parseInt(starSize * maxStar + gutter * (maxStar-1));//최대 마스크 너비 계산
    document.querySelector('input[name=ratevalue]').max = maxStar;//입력 필드 최대값 재설정
            
    //별점 숫자 입력 값 변경 이벤트 리스너
    document.querySelector('input[name=ratevalue]').addEventListener('change',(e)=>{
    })

    //마우스 클릭 별점 변경 이벤트 리스너
    document.querySelector('.rating').addEventListener('click',(e)=>{
    })            
})

기본 환경 변수 값은 CSS 변수로 설정해도 되지만 구현의 편의를 위해 자바스크립트 변수로 설정합니다.

사용한 변수는 다음과 같은 역할을 합니다.

1. starSize : 별 이미지 1개의 가로/세로 크기입니다. 별 이미지 1개는 정사각형 크기로 정의되고 그 안에 SVG 이미지가 1:1 크기로 채워져 표시됩니다.

관리의 편의를 위해 별 이미지는 <div> 태그로 감싸서 <div> 태그에 starSize 크기를 적용합니다.

2. maxStar : 별 이미지 최대 개수를 정합니다. 원하는 최대 별점 개수를 정하면 자동으로 거기에 맞춰 최대 별점 개수가 표시됩니다. 입력 필드의 최대 값도 maxStar 변수값으로 다시 초기화를 해야 입력 필드의 값 변화 범위가 변경됩니다.

3. gutter :  별 사이의 여백을 설정합니다. 별 이미지 사이에 공간이 없으면 마스크 오버레이 크기 값 계산이 단순하고 쉬워지는 장점이 있지만, 현실 세계에서는 그렇지 않기 때문에 별 이미지 사이의 여백 값을 설정해서, 오버레이 마스크 너비를 계산할 때도 반영해야 합니다.

4. maskMax : 오버레이 마스크의 최대 너비 값입니다. ".rating" 클래스를 담고 있는 <div> 태그의 너비와 같습니다. 초기화를 할 때 처음 한번 계산해서 사용하며, 오버레이 마스크 너비를 계산할 때 반복적으로 사용되기 때문에 별도의 변수로 선언해서 관리를 합니다.


5. 입력 필드 값 변경에 따라 별점이 변하도록 계산 코드 추가

코드는 단순합니다. 입력 필드의 별점 값이 변경되면, 입력된 값으로 마스크 오버레이 크기를 계산해서 너비 값만 변경해주면 됩니다.

주의해야 할 점이 있습니다. 

위 CSS 구현을 다시 보면 마스크 오버레이가 별점 영역 오른쪽 끝에 붙어있습니다.(right: 0)

.overlay {
    background-color: #000;
    opacity: 0.5;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 1;
    transition: all 0.3s ease-in-out;
}

마스크 오버레이는 색상으로 채워진 별점 영역을 표시하는 것이 아니라 채워지지 않은 영역을 표시하는 용도로 사용합니다.

따라서 마스크 오버레이의 너비는 별점 숫자가 높아지면 반대로 너비가 줄어들어야 합니다.

따라서 별점 전체 영역 너비에서 별점이 채워진 너비를 뺀 나머지 너비가 마스크 오버레이가 차지하는 너비가 됩니다.

그리고, 별 이미지 사이의 여백(gutter - 거터)까지 계산해 포함해서 마스크 오버레이 너비를 계산해야 합니다.

계산식이 다소 복잡해 보이는 것은 마스크 오버레이와 거터 계산을 구분해서 표시하기 위한 것입니다.

별점 전체 영역에서 별점만큼의 채워진 별 이미지들 너비를 빼고, 추가로 여백 개수(별점의 소수점을 버린 정수 값 - Math.floor() 함수로 구함) 만큼 여백을 빼주면 됩니다.

maskMax - val * starSize - Math.floor(val) * gutter

별점 전체 영역 - ( 별점 x 별 1개 너비 ) - (별점의 정수 값만 x 여백 1개 크기)

document.querySelector('input[name=ratevalue]').addEventListener('change',(e)=>{
    const val = e.target.value;
    //계산식 - 마스크 최대 너비에서 별점x별크기를 빼고, 추가로 별점 버림 정수값x거터 크기를 빼서 마스크 너비를 맞춤
    document.querySelector('.overlay').style.width = parseInt(maskMax - val * starSize - Math.floor(val) * gutter) + 'px';//마스크 크기 변경해서 별점 마킹
})

여기까지 완성해서 동작시켜보면 HTML 코드에서 별점 초기값으로 정한 3이 적용되지 않는 걸 알 수 있습니다.

입력 필드의 별점 값을 변경하면 이벤트 리스너가 호출되면서 비로소 별점 값이 적용됩니다.

초기화 단계에서 HTML 입력 필드의 값을 읽어서 별점을 마킹해주는 코드가 추가로 있어야 초기화된 별점을 볼 수 있습니다. 입력 필드의 초기값을 0으로 해놓으면 되지만, 초기 값이 5일 수도 있으므로 초기화는 해야 합니다.

오버레이 마스크 값을 계산하는 행을 별도의 함수로 분리해서 다음과 같이 작성합니다.

document.querySelector('input[name=ratevalue]').addEventListener('change',(e)=>{
    const val = e.target.value;
    //계산식 - 마스크 최대 너비에서 별점x별크기를 빼고, 추가로 별점 버림 정수값x거터 크기를 빼서 마스크 너비를 맞춤
    setRating(val);
})

//별점 마킹 함수
function setRating(val = 0){
    document.querySelector('.overlay').style.width = parseInt(maskMax - val * starSize - Math.floor(val) * gutter) + 'px';//마스크 크기 변경해서 별점 마킹
}

그리고 'DOMContentLoaded' 초기화 함수 안에서 다음과 같이 setRating() 함수를 호출해서 초기화를 해주면 됩니다.

setRating(document.querySelector('input[name=ratevalue]').value);

6. 마우스 별점 클릭에 따라 별점 및 입력 필드가 변하도록 계산 코드 추가

마우스 클릭한 위치를 기준으로 별점을 계산하는 이벤트 리스너는 3행이지만 중요하게 알아야 할 내용들이 여러 가지 있습니다.

마스크 오버레이 크기는 전체 별점 영역 너비 값에서 별점 영역을 감싸는 태그의 왼쪽 끝에서 부터 클릭한 위치까지의 거리를 계산해서 빼면 쉽게 구할 수 있습니다.

이벤트 리스너에서 받은 클릭 이벤트가 발생한 X 좌표(e.clientX)는 웹 브라우저의 도큐먼트 영역 왼쪽 끝에서부터의 거리이기 때문에 별점 영역 전체를 감싸는 태그(".rating")의 왼쪽 시작 위치만큼을 빼야 별점 영역 전체를 감싸는 태그 안의 왼쪽 끝에서부터 클릭한 위치까지의 실제 거리를 얻을 수 있습니다.

document.querySelector('.rating').addEventListener('click',(e)=>{
    //closest()로 .rating 요소의 왼쪽 위치를 찾아서 현재 클릭한 위치에서 빼야 상대 클릭 위치를 찾을 수 있음.
    const maskSize = parseInt(maskMax - parseInt(e.clientX) + e.target.closest('.rating').getBoundingClientRect().left);//클릭한 위치 기준 마스크 크기 재계산
    document.querySelector('.overlay').style.width = maskSize + 'px'; //오버레이 마스크 크기 변경해서 별점 마킹
    document.querySelector('input[name=ratevalue]').value = Math.floor((maskMax - maskSize) / (starSize + gutter)) + parseFloat(((maskMax - maskSize) % (starSize + gutter) / starSize).toFixed(1));
})

클릭한 위치 정보를 얻으려면 다음 코드를 이해해야 합니다. 별점 영역을 감싸는 태그(".rating")가 도큐먼트 영역 왼쪽 끝에서 얼마나 떨어져 있는지를 얻는 코드입니다.

e.target.closest('.rating').getBoundingClientRect().left

e.target 클릭한 요소
closest('.rating') 클릭한 요소 상위의 ".rating" 클래스를 가진 요소를 찾음(별점 영역을 감싼 태그)
getBoundClientRect() ".rating" 클래스 태그의 위치 정보를 얻음
left ".rating" 클래스 태그가 도큐먼트 왼쪽 끝에서부터 얼마나 떨어져있는지를 가진 값

이렇게 위치를 구해야 하는 이유는 다음 글을 보면 도움이 됩니다.

> 이벤트 캡쳐링(Capturing)과 버블링(Bubbling)의 이해

실제로 구현할 때는 조금 더 간결하고 축약된 자바스크립트 코드를 사용하게 됩니다.

구현하는 개념을 설명하기 위해서 풀어서 작성한 코드들이 더러 있는 점을 감안해서 사용하는 것을 추천합니다.

별점 입력 필드의 별점 값을 업데이트하려면 별점 값을 계산해야 합니다.

마스크 오버레이 너비를 이미 알고 있으므로, 전체 별점 영역 너비에서 마스크 오버레이 너비를 뺀 후 별 1개 너비로 나누면 별점을 구할 수 있습니다.

별점 계산 방법

이때도 마찬가지로 별 이미지 사이의 여백이 계산에 영향을 미치므로 주의해야 합니다.

별점 계산은 정수 부분과 소수점 부분 2 부분으로 나누어서 구해야 합니다.

별점의 정수 부분은 ( 별 이미지 1개 너비 + 사이 여백 ) 값으로 나눈 후 버림을 하면(Math.floor()) 구할 수 있습니다.

Math.floor((maskMax - maskSize) / (starSize + gutter))

소수점 부분은 ( 별 이미지 1개 너비 + 사이 여백 ) 값으로 나눈 나머지 값을 별 이미지 1개 너비로 나누면 구할 수 있습니다.

입력 필드는 소수점 첫 째 자리까지만 표시를 하므로 소수점 첫 째 자리에서 잘라낸 후(toFixed(1)) 다시 실수로 변경합니다.

parseFloat(((maskMax - maskSize) % (starSize + gutter) / starSize).toFixed(1))