이전포스트에서는 carousel slide의 이미지 크기를 고치는 과정을 소개했다. 이번에는 라이브러리를 통한 구현에서 직접구현까지의 과정을 소개해본다
내가 원하는 carousel slide는 반응형이었다. viweport를 기준으로 반응형을 만드는 것은 어느정도 익숙 했지만 포트폴리오의 경우 viewport 안에서 내가 만든 browser 컴포넌트를 기준으로 반응해야 했기에 초반에는 어떻게 구현할지 감이 잘 오지 않았다. 그래서 반응형 carousel slide를 지원하는 라이브러리를 사용하기로 했다.
여러 라이브러리 중에서 react-slick을 선택한 이유는 커스터마이징이 어렵지 않았기 때문이었다.
Slider 태그에 settings 값으로도 어느정도 원하는 결과를 얻을 수 있었다.
나의 경우에는 화살표 버튼만 삭제하는 방식으로 settings 값을 주어서 구현했다.
function CarouselSlide({images}: {images:string[]}) {
const settings = {
arrows: false,
dots: true,
infinite: false,
slidesToShow: 1,
slidesToScroll: 1,
variableWidth: true,
rows: 1,
slidesPerRow: 1
}
return (
<Slider {...settings}>
{images.map((img) => (
<div
key={img}
>
<Image
src={img}
/>
</div>
))}
</Slider>
)
}
위의 단점들로 인해서 carousel을 최적화 시킬 목적으로 직접 구현하기로 결정했다.
crousel을 직접 구현하기 위해서 필요한 요소들은 크게 3가지 정도이다.
이미지가 보여질 div
크기를 설정하고 overflow: hidden 값을 주어 벗어나는 요소들을 가린다.
보여줄 이미지의 index
화면에 나타날 이미지의 index값을 선언해서 원하는 이미지를 보여준다
가로로 이어붙인 이미지
display: flex의 div에 이미지들을 넣어준다. div에는 translate-x에 index을 통해 계산한 값을 넣어서 좌우로 움직이게 만들어 준다.
function CarouserlSlide({images}: {images:string[]}) {
//... size = 상위 div width, height
const [currentIndex, setCurrentIndex] = useState(0);
return (
<div className="w-full overflow-hidden h-max">
<div
className="flex items-center h-max"
style={{
transform: `translateX(${-currentIndex * size.w}px)`,
transition: `transform 300ms ease-in-out 0s`,
}}
>
{images.map((img, i) => (
<Image
key={i + img}
src={img}
alt={`image${i}`}
width={size.w}
height={size.h}
priority
draggable={false}
/>
))}
</div>
</div>
)
}
이런 기본적인 틀에 내가 원하는 요소들을 추가했다.
무한히 순환하는 slide
한쪽으로만 움직여도 slide 전체를 볼 수 있도록 기존 images의 앞뒤에 마지막과 처음 이미지를 넣고 index가 0이거나 length-1일 경우 1이나 length-2로 index를 변경해준다.
//...
const slideList = [images.at(-1), ...images, images.at(0)]
const onTransitionEnd = () => { // transition이 끝난 후 발동
if (currentIndex === 0) {
setCurrentIndex(slideList.length - 2);
} else if (currentIndex === slideList.length - 1) {
setCurrentIndex(1);
}
}
//...
<div
className="flex items-center h-max"
style={{
transform: `translateX(${-currentIndex * size.w}px)`,
transition: `transform 300ms ease-in-out 0s`,
}}
onTransitionEnd={onTransitionEnd}
>
hover 시에 보이는 arrow 영역
tailwind를 사용했기 때문에 group 속성을 통해 최상위 오른쪽 왼쪽 1/6 지점에 hover 되었을 때 나타나도록 만들어 줬다.
<div
className='absolute top-0 h-[calc(100%-35px)] flex items-center justify-center w-1/6 group hover:bg-[rgba(114,114,114,0.2)] '
onClick={()=>setCurrentIndex(currentIndex-1)}
>
<BsChevronDoubleLeft size=40 color="rgb(78, 78, 78)" className='hidden group-hover:block'/>
</div>
dot을 통해 현재 이미지 위치 확인
currentIndex를 통해 이미지의 현재 위치를 표시하고 onclick event를 통해 조작할 수 있게 만들었다.
<div className="flex items-center mx-auto w-max">
{images.map((img, i) => (
<div key={img + i}>
<BsDot
size={35}
color={
currentIndex === i + 1 ? "rgb(41, 41, 41)" : "rgb(177, 177, 177)"
}
onClick={() => setCurrentIndex(i + 1)}
className="cursor-pointer"
/>
</div>
))}
</div>
drag로 slide 조작
가장 핵심적인 기능으로 이 블로그를 통해 drag event를 만들어 구현했다. (그는 신이야...)
구현을 위해 여러 블로그를 읽어보던 도중에 위의 블로그를 통해 미쳐 생각 못한 조건부로 animation을 넣는것을 깨달아서 정말 많은 도움이 되었다.
// sliderWidth : 보여지는 부분의 넓이
const [transX, setTransX] = useState(0); // 이동 거리
const [animation, setAnimation] = useState(false); // animation 트리거
const inrange = (n, min, max) => {
if(n<min) return min
if(n>max) return max
return n
}
const onMouseDown = (clickEvent: React.MouseEvent<Element, MouseEvent>) => {
const onDragChange = (intervalX: number) => {
setTransX(inrange(intervalX, -sliderWidth + 10, sliderWidth - 10));
};
const onDragEnd = (intervalX: number) => {
if (intervalX < -90) {
setCurrentIndex(currentIndex+1)
}
if (intervalX > 90) {
setCurrentIndex(currentIndex-1)
}
setAnimation(true);
setTransX(0);
};
const mouseMoveHandler = (moveEvent: MouseEvent) => {
const deltaX = moveEvent.pageX - clickEvent.pageX; // 마우스가 움직인 위치 - 마우스 시작 위치
onDragChange(deltaX);
};
const mouseUpHandler = (moveEvent: MouseEvent) => {
const deltaX = moveEvent.pageX - clickEvent.pageX;
onDragEnd(deltaX);
document.removeEventListener("mousemove", mouseMoveHandler);
};
document.addEventListener("mousemove", mouseMoveHandler);
// 전역으로 enevt를 주는 이유는 마우스가 div의 크기를 벗어났을 때에도 이번트가 계속 이어지길 바라기 때문이다.
// 안그러면 div를 벗어났을때 중간에 멈춘듯이 작동하며, event가 이어지지도 않는다.
document.addEventListener("mouseup", mouseUpHandler, { once: true });
}
const onTransitionEnd = () => {
setAnimation(false) // 이미지가 이동한 후 animation 종료
//...
}
//...
<div
className="flex items-center h-max"
style={{
transform: `translateX(${-currentIndex * size.w + transX}px)`,
transition: `transform ${animation ? 300 : 0}ms ease-in-out 0s`,
}}
onTransitionEnd={onTransitionEnd}
onMouseDown={onMouseDown}
>{...}</div>
이렇게 해서 carousel slide를 라이브러리로 구현 했다가 직접 구현으로 변경하였다.
이전에는 못해봤던 부분을 구현해본 경험이라 mouseEvent, 조건부 trasition 타이밍 등 여럿 배운 부분이 많았다. 다른 사람이 공유한 지식을 통해 내가 원하는 것을 만들 수 있었기에 나도 누군가에게 도움이 되는 글을 적게 되기를 바란다