개발 기술/사소하지만 놓치기 쉬운 개발 지식

[CSS] clip-path로 별점(Rating) UI 구현하는 법 (with. vue 컴포넌트 예시 포함)

by GicoMomg 2024. 8. 28.

1. 들어가며…

  • 이번 시간에는 별점을 매기는 Rating 기능을 구현하는 법을 알아보았다.
  • Rating을 구현할 때 키 포인트는 (1) 빈 별 & 색이 차있는 별를 겹쳐야 하며, (2) clip-path로 색이 차 있는 별 일부를 보여줘야 한다.
  • 해당 포스팅을 다 읽고 나면, css의 clip-path 사용법과 Rating 기능 구현을 할 수 있게 된다.



1) clip-path는 무엇일까?

clip-path는 CSS 속성 중 하나로, 요소의 일부분을 잘라내거나 요소의 특정 영역만 표시할 때 쓴다.

.element {
    clip-path: <shape> <coordinates>;
}
  • <shape>: 요소를 자르는 도형의 형태(ex. clircle, ploygon, inset 등)
  • <coordinates>: 도형의 위치와 크기를 결정하는 좌표 값

 

(1) 속성 살펴보기

  • circle()은 원형으로 요소를 자를 때 쓴다.
  • 만약 아래와 같이 clip-path를 선언하면, 요소 중앙에 반지름이 50%인 원 영역 만큼만 요소를 보여준다.
/* 용법 */
clip-path: circle(반지름_크기 at x축 y축)

/* 예시 */
clip-path: circle(50% at 50% 50%); 

 



  • polygon()은 다각형 형태로 요소를 자를 때 쓰인다. ploygon()은 여러 개의 좌표를 선언할 수 있다.
  • 만약 ploygon()을 사용하면, 말풍선이나 별 모양 등 다양한 형태로 요소를 자를 수 있다.
  • 아래 예시처럼 선언하면, 삼각형 모양으로 요소가 잘린다.
/* 용법 */
clip-path: polygon(첫 번째 좌표x y, 두 번째 좌표x y, ...)

/* 예시 */
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);

 



  • inset()은 사각형 모양으로 요소를 자를 수 있다.
  • ploygon()과 달리, 오직 직사각형, 정사각형 형태만 만들 수 있다.
  • 좌표는 요소의 위를 기준으로 시계방향으로 선언한다.
/* 용법 */
clip-path: inset(첫 번째 좌표, 두 번째 좌표, ...)

/* 예시 */
clip-path: inset(10% 20% 30% 40%);

 



  • ellipse() 은 타원형으로 요소를 자를 수 있다.
  • 아래 코드처럼 선언하면, 요소의 중앙에서 가로가 50%, 세로가 25%인 타원 모양으로 요소를 자른다.
/* 용법 */
clip-path: ellipse(수평_반지름 수직_반지름 at x축 y축)

/* 예시 */
clip-path: ellipse(50% 25% at 50% 50%);

 



  • 마지막으로 path()은 커스텀 경로를 사용해 자를 모양을 정의할 수 있다.
  • 또한, SVG의 경로 데이터를 그대로 사용할 수 있다.
/* 용법 */
clip-path: ellipse(수평_반지름 수직_반지름 at x축 y축)

/* 예시 */
clip-path: path('M10 10 H 90 V 90 H 10 Z');

 

💡 이처럼 clip-path은 여러가지 모양으로 요소를 자를 수 있다.
앞서 보여준 gif의 경우, Clippy사이트에서 테스트한 예시인데 여러가지 형태를 더 보고 싶거나 용법이 이해되지 않는다면 추천한다.

  • 이제 clip-path가 무엇인지 알았으니, 본격적으로 clip-path를 사용해 Rating기능을 구현해보자!




2. Clip-Path로 Rating 구현하는 법

1) 미리보기

See the Pen rating by KumJungMin (@kumjungmin) on CodePen.

 

2) html

  • html 구조는 단순한데…
  • 그 이유는 js로 .rating 요소에 빈 별과 채워진 별을 각각 5개씩 렌더링하기 때문이다.
<div class="rating" id="rating"></div>

 

3) CSS

💡 CSS 코드를 살펴보기 전에, 요소의 구조를 살펴보자.

  • 우선 JS단에서 .rating에 총 5개의 .star를 자식으로 선언한다.

 

  • 선언된 .star.star-empty (빈 별 아이콘)와 .star-filled (채워진 별 아이콘)가 있는 구조이다.
  • 그리고 현재 별점을 나타낼 때는, clip-path를 사용해 채워진 별 아이콘(.star-filled)을 일부만 보여주는 형태이다.

  • 그럼, 실제로 어떻게 구현되었는지 CSS, JS코드를 살펴보자!

 

(1) 전체코드 보기

.rating {
  width: fit-content;
  display: flex;
  cursor: pointer;
  gap: 2px;
  justify-content: center;
  margin: 50px auto 0;
}

/* js로 주입한 star 요소에 대한 스타일 */
.star {
  position: relative;
  width: 30px;
  height: 30px;
  transition: transform 0.2s ease;
}

.star:hover,
.star.filled {
  transform: scale(1.2);
}

.star-empty,
.star-fill {
  position: absolute;
  top: 0;
  left: 0;
  width: 30px;
  height: 30px;
  background-size: contain;
  background-repeat: no-repeat;
  transition: clip-path 0.2s ease;
}

.star-empty {
  background-image: url('data:image/svg+xml;...'); 
}

.star-fill {
  background-image: url('data:image/svg+xml;base64...'); 
  clip-path: inset(0 100% 0 0);
}

 

(2) rating 규격 설정

.rating {
  width: fit-content;
  display: flex;
  cursor: pointer;
  gap: 2px;
  justify-content: center;
  margin: 10px auto 0;
}
  • width: fit-content: 컨텐츠의 크기에 맞게 요소의 너비가 자동으로 조정한다.
  • display: flex: 내부에 있는 요소들이 플렉스 컨테이너로 정렬한다.
  • gap: 2px: 별 요소들 사이의 간격을 2px로 지정한다.
  • cursor: pointer: 사용자가 요소 위에 마우스를 올렸을 때 커서가 손 모양으로 변경한다.
  • justify-content: center: 내부의 별 요소들이 수평으로 가운데 정렬한다.

 

(3) .star 요소의 스타일

.star {
  position: relative;
  width: 30px;
  height: 30px;
  transition: transform 0.2s ease;
}
  • position: relative: 내부의 자식 요소들이 이 부모 요소를 기준으로 절대 위치를 가질 수 있게 한다.
  • widthheight: 각 별의 크기를 30px로 설정한다.
  • transition: transform 0.2s ease: 마우스 호버시 별이 커지는 애니메이션이 0.2초 동안 진행된다.

 

(4) .star, .star.filled 클래스

.star:hover,
.star.filled {
  transform: scale(1.2);
}
  • transform: scale(1.2): 마우스를 올리거나, 별이 채워진 상태인 경우 요소의 크기가 1.2배로 커진다.

 

(5) .star-empty, .star-fill 클래스

.star-empty,
.star-fill {
  position: absolute;
  top: 0;
  left: 0;
  width: 30px;
  height: 30px;
  background-size: contain;
  background-repeat: no-repeat;
  transition: clip-path 0.2s ease;
}
  • position: absolute: 이 요소들이 부모 요소(.star) 안에서 절대 위치를 가진다.
  • top: 0; left: 0: 부모 요소의 좌상단에 배치된다.
  • background-size: contain: 배경 이미지가 요소 크기에 맞게 조정되며, 비율을 유지된다.
  • background-repeat: no-repeat: 배경 이미지가 반복되지 않는다.
  • transition: clip-path 0.2s ease: clip-path 속성이 변경될 때 0.2초 동안 애니메이션이 진행된다.

 

(6) .star-empty 클래스

.star-empty {
  background-image: url('data:image/svg+xml;base64,...');
  • background-image: 빈 별의 이미지를 인라인 SVG 데이터로 설정한다.

 

(7) .star-fill 클래스

css코드 복사
.star-fill {
  background-image: url('data:image/svg+xml;base64,...');
  clip-path: inset(0 100% 0 0);
}
  • background-image: 채워진 별의 이미지를 인라인 SVG 데이터로 설정한다.
  • clip-path: inset(0 100% 0 0): 채워진 별의 이미지를 가리기 위해 clip-path를 사용한다. 이 속성값은 JS에서 별점값에 따라 변경된 예정이다.



4) JS

(1) 전체 코드 보기

const RATING_COUNT = 5;;
let currentRating = 0;
let hoveredRating = 0;

const rating = document.getElementById('rating');

const updateStars = (value) => {
  const stars = rating.children;
  for (let i = 0; i < RATING_COUNT; i++) {
    const fillPercentage = getClipPathPercent(i + 1, value);
    stars[i].querySelector('.star-fill').style.clipPath = `inset(0 ${fillPercentage}% 0 0)`;
    if (fillPercentage < 100) {
      stars[i].classList.add('filled');
    } else {
      stars[i].classList.remove('filled');
    }
  }
};

const createStarElement = () => {
  const star = document.createElement('div');
  star.className = 'star';

  const starEmpty = document.createElement('div');
  starEmpty.className = 'star-empty';
  star.appendChild(starEmpty);

  const starFill = document.createElement('div');
  starFill.className = 'star-fill';
  star.appendChild(starFill);

  return star;
};

const getClipPathPercent = (starIndex, value) => {
  if (starIndex <= value) return 0;
  return (starIndex - value === 0.5) ? 50 : 100;
};

const handleMouseMove = (e) => {
  const rect = rating.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const width = rect.width;

  const value = Math.ceil((x / width) * RATING_COUNT * 2) / 2;
  hoveredRating = Math.min(Math.max(value, 0.5), RATING_COUNT);

  updateStars(hoveredRating);
};

const fixRating = () => {
  if (hoveredRating === 0) return;

  currentRating = hoveredRating;
  updateStars(currentRating);
};

const resetRating = () => {
  hoveredRating = 0;
  updateStars(currentRating);
};

rating.addEventListener('mousemove', handleMouseMove);
rating.addEventListener('mouseleave', resetRating);
rating.addEventListener('click', fixRating);

for (let i = 0; i < RATING_COUNT; i++) {
  rating.appendChild(createStarElement());
}

updateStars(currentRating);

 

(2) 초기 설정하기

const RATING_COUNT = 5;
let currentRating = 0;
let hoveredRating = 0;

const rating = document.getElementById('rating');
  • RATING_COUNT는 별의 개수(5개)를 정의한다.
  • currentRating은 사용자가 클릭하여 선택한 현재의 평점을 저장한다.
  • hoveredRating은 마우스를 올렸을 때의 임시 평점을 저장한다.
  • rating은 HTML에서 별점을 표시할 요소를 가져온다.

 

(3) 별 업데이트 함수

const updateStars = (value) => {
  const stars = rating.children;

  for (let i = 0; i < RATING_COUNT; i++) {
    const fillPercentage = getClipPathPercent(i + 1, value);
    stars[i].querySelector('.star-fill').style.clipPath = `inset(0 ${fillPercentage}% 0 0)`;
    if (fillPercentage < 100) {
      stars[i].classList.add('filled');
    } else {
      stars[i].classList.remove('filled');
    }
  }
};

updateStars(currentRating); // 최초 1번 실행
  • updateStars 함수는 별점 요소들을 업데이트한다.
  • 전달된 value에 따라 각 별의 clip-path를 조정하여, 별이 부분적으로 채우거나 완전히 채워지거나, 혹은 비워지도록 한다.
  • 최초 1번 실행해, 주어진 별점 값에 따라 clip-path를 조정한다.

 

(4) 별 생성 함수

const createStarElement = () => {
  const star = document.createElement('div');
  star.className = 'star';

  const starEmpty = document.createElement('div');
  starEmpty.className = 'star-empty';
  star.appendChild(starEmpty);

  const starFill = document.createElement('div');
  starFill.className = 'star-fill';
  star.appendChild(starFill);

  return star;
};
  • createStarElement 함수는 빈 별 & 채워진 별을 자식으로 가지는, 별(.star) 요소를 생성한다.
  • .star-empty은 빈 별을 배경이미지를 가지고, .star-fill은 채워진 별을 배경 이미지로 가진다.

 

for (let i = 0; i < RATING_COUNT; i++) {
  rating.appendChild(createStarElement());
}
  • 우리는 총 5개의 별이 필요하므로, createStarElement을 5번 반복해 html에 주입한다.

 

(5) clip-path 경로 계산 함수

const getClipPathPercent = (starIndex, value) => {
  if (starIndex <= value) return 0;
  return (starIndex - value === 0.5) ? 50 : 100;
};
  • getClipPathPercent 함수는 각 별의 채워짐 정도를 계산한다.
  • 정수 또는 0.5 단위의 값에 따라 별이 완전히 또는 부분적으로 채워진다.

 

(6) 마우스 움직임 처리 함수

const handleMouseMove = (e) => {
  const rect = rating.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const width = rect.width;

  const value = Math.ceil((x / width) * RATING_COUNT * 2) / 2;
  hoveredRating = Math.min(Math.max(value, 0.5), RATING_COUNT);

  updateStars(hoveredRating);
};

rating.addEventListener('mousemove', handleMouseMove);
  • 마우스가 별점 요소 위에서 움직일 때, 마우스 위치에 따라 별의 임시 평점을 계산한다.
  • 임시 평점은 hoveredRating에 저장되며, 마우스 hover시 별점 아이콘에 호버된 영역이 반영된다.

 

(7) 클릭 처리 함수

const fixRating = () => {
  if (hoveredRating === 0) return;

  currentRating = hoveredRating;
  updateStars(currentRating);
};

rating.addEventListener('click', fixRating);
  • 사용자가 별을 클릭했을 때 현재의 hoveredRating 값을 currentRating에 저장하고,
  • 그 값을 기반으로 채워진 별의 clip-path을 업데이트한다.

 

(8) 마우스가 벗어났을 때 처리 함수

const resetRating = () => {
  hoveredRating = 0;
  updateStars(currentRating);
};

rating.addEventListener('mouseleave', resetRating);
  • 마우스가 별점 요소를 벗어났을 때 hoveredRating 값을 0으로 초기화하고,
  • 마지막으로 클릭된 평점(currentRating)을 다시 화면에 표시한다.



💡 앞선 일련의 과정을 거치면…
- 사용자는 마우스 클릭으로 별점을 지정할 수 있고,
- 마우스 호버에 따라 별점이 변경되며,
- 마우스가 영역을 떠났을 때 마지막으로 클릭했던 별점으로 초기화된다.




3. 마치며…

  • 이번 시간에는 clip-path를 사용해 Rating 기능을 구현해보았다. clip-path는 특정 형태로 요소를 보여줄 수 있는 기능으로 해당 포스팅에서는 별점 요소를 숨기기 위해 사용했다. 해당 포스팅에서는 바닐라 JS로 구현했으나 Vue로 구현하게 되면, 아이콘을 props로 더 쉽게 커스텀할 수 있다. 만약 Vue 예시를 보고 싶다면, Vue로 구현한 예시는 이 사이트에서 볼 수 있다.

 

반응형

댓글