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%);
- 좀 더 쉽게 이해하기 위해 Clippy사이트의 예시를 보자.
polygon()
은 다각형 형태로 요소를 자를 때 쓰인다.ploygon()
은 여러 개의 좌표를 선언할 수 있다.- 만약
ploygon()
을 사용하면, 말풍선이나 별 모양 등 다양한 형태로 요소를 자를 수 있다. - 아래 예시처럼 선언하면, 삼각형 모양으로 요소가 잘린다.
/* 용법 */
clip-path: polygon(첫 번째 좌표x y, 두 번째 좌표x y, ...)
/* 예시 */
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
- Clippy사이트에서
polygon
을 테스트한 모습이다.
inset()
은 사각형 모양으로 요소를 자를 수 있다.ploygon()
과 달리, 오직 직사각형, 정사각형 형태만 만들 수 있다.- 좌표는 요소의 위를 기준으로 시계방향으로 선언한다.
/* 용법 */
clip-path: inset(첫 번째 좌표, 두 번째 좌표, ...)
/* 예시 */
clip-path: inset(10% 20% 30% 40%);
- Clippy사이트의 inset 예시를 보자.
ellipse()
은 타원형으로 요소를 자를 수 있다.- 아래 코드처럼 선언하면, 요소의 중앙에서 가로가 50%, 세로가 25%인 타원 모양으로 요소를 자른다.
/* 용법 */
clip-path: ellipse(수평_반지름 수직_반지름 at x축 y축)
/* 예시 */
clip-path: ellipse(50% 25% at 50% 50%);
- Clippy사이트의 ellipse 예시를 보자.
- 마지막으로
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
: 내부의 자식 요소들이 이 부모 요소를 기준으로 절대 위치를 가질 수 있게 한다.width
와height
: 각 별의 크기를 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로 구현한 예시는 이 사이트에서 볼 수 있다.
반응형
'개발 기술 > 사소하지만 놓치기 쉬운 개발 지식' 카테고리의 다른 글
[CSS/JS] ellipsis 말줄임에서 발생할 수 있는 2가지 인터렉션 문제 (0) | 2024.09.19 |
---|---|
[JS] event.target vs event.currentTarget의 차이 (with. vue slot) (0) | 2024.09.08 |
[JS] RangeSlider를 구현하는 법(feat. 중첩 range input) (2) | 2024.07.14 |
[JS] 모달 창 띄울 때 레이아웃 깨짐 없이 스크롤 막는 법 (0) | 2024.06.11 |
[TS] 입력 유효성을 체크하는 여러 정규식(feat. 한글, 전화번호, 사업자등록번호, 이메일 등) (0) | 2024.05.13 |
댓글