메인 페이지에 슬라이드 배너 (캐로셀)을 react-slick이라는 라이브러리를 이용해 구현했었는데,
1. 디자인 커스텀이 어렵고
2. 라이브러리를 많이 설치하면 성능 상 좋지 않을 것 같아 직접 구현해보기로 했다.
기본 원리
구현하는 것은 어렵지 않은데,
- 슬라이더에 넣고 싶은 이미지를 flex를 이용해 가로로 길게 붙이고
- useEffect로 처음 렌더링 되었을 때 setInterval을 실행시켜, 5초마다 옆으로 이동시켜준다
- 이동 버튼을 누를 경우, 옆으로 이동시켜준다.
개인적으로 배너 위에 마우스를 올렸을 때 좌우 이동 버튼이 나타나는게 예쁜 것 같아서 버튼 hover도 같이 구현했다.
영상에는 무한 루프 슬라이더가 구현되어있는데, 이번 게시글 코드에는 구현되지 않고 다음 게시글에서 마저 구현할 예정이다.
전체 코드
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
const images = [
{ src: "/image/sample/1.jpg" },
{ src: "/image/sample/2.jpg" },
{ src: "/image/sample/3.jpg" },
{ src: "/image/sample/4.jpg" },
{ src: "/image/sample/1.jpg" },
];
const Carousel = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const [isHovered, setIsHovered] = useState(false);
const slideButtonStyle = `w-14 h-14 text-xl absolute top-1/2 transform -translate-y-1/2 bg-white shadow-md p-2 rounded-full transition-opacity duration-100`;
const slideNext = () => {
setCurrentIndex((prevIndex) =>
prevIndex === images.length - 1 ? 0 : prevIndex + 1
);
};
const slidePrev = () => {
setCurrentIndex((prevIndex) =>
prevIndex === 0 ? images.length - 1 : prevIndex - 1
);
};
useEffect(() => {
const interval = setInterval(slideNext, 5000);
return () => clearInterval(interval);
}, []);
return (
<div className="mx-auto">
<div
className="mt-12 relative mx-5 max-w-[1800px] h-[300px] md:h-[400px] lg:h-[500px] overflow-hidden rounded-3xl"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className="flex transition-transform duration-300 ease-in-out h-full w-full"
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
>
{images.map((image, index) => (
<div key={index} className="w-full shrink-0 relative">
<Image
key={index}
src={image.src}
alt={`Slide ${index}`}
className="object-cover"
width={1800}
height={400}
priority={index === currentIndex}
/>
</div>
))}
</div>
<button
onClick={slidePrev}
className={`left-4 ${slideButtonStyle} ${
isHovered ? "opacity-100" : "opacity-0"
}`}
>
❮
</button>
<button
onClick={slideNext}
className={`right-4 ${slideButtonStyle} ${
isHovered ? "opacity-100" : "opacity-0"
}`}
>
❯
</button>
</div>
<div className="absolute mt-2 p-3 left-1/2 transform -translate-x-1/2 flex space-x-2">
{images.map((_, index: number) => (
<button
key={index}
className={`h-2 rounded-full duration-300 ease-in-out ${
index === currentIndex ? "w-6 bg-black" : "w-2 bg-gray-300"
}`}
onClick={() => setCurrentIndex(index)}
/>
))}
</div>
</div>
);
};
export default Carousel;
1. slideNext나 slidePrev에서 currentIndex를 제어한다.
2. 캐러셀 사진을 담고있는 컨테이너가 currentIndex에 따라 좌우로 이동한다. 이때 transform translate를 이용해 이동하고, duration으로 애니메이션에 시간을 준다.
3. 버튼에 slideNext와 slidePrev 함수를 연결한다.
onMouseEnter, onMouseLeave
마우스가 hover되었을 때 isHovered state를 변경한다.
setInterval
5초마다 이미지를 넘겨주기 위해 setInterval을 이용했다.
useEffect를 이용해 컴포넌트가 처음 렌더링 될 때 함수가 5초에 한번씩 실행되도록 설정하고, 언마운트될 때 클리어해서 메모리 누수를 방지한다.
useState에서(prevIndex)⇒prevIndex+1
처음에는 setCurrentIndex((prevIndex) => prevIndex === 0 ? images.length - 1 : prevIndex - 1); 이 아니라 setCurrentIndex(currentIndex=== 0 ? images.length - 1 : currentIndex- 1); 로 구현했다.
문제상황
그런데, 처음에 index가 1번인 상태에서 이동 버튼을 눌러 index가 3이 된 후에도, 5초가 지나면 index가 2로 변해버렸다. 왜이럴까..?
const interval = setInterval(slideNext, 5000);의 setInterval 함수가 5초 후에 slideNext를 실행시키는게 아니라, slideNext가 실행되기 5초 전 함수와 내부의 state를 저장해두고, 5초 후에 실행시키는게 아닐까? 라는 생각이 들어서 useState의 작동 원리에 대해 알아보았다.
이유
리액트 공식 문서에서 답을 찾을 수 있었다.
https://ko.react.dev/learn/state-as-a-snapshot
state는 스냅샷처럼 동작한다. (함수 실행 시점의 state를 저장해서 함수 실행->state 바꿔 끼움)
함수 실행 중에 state가 변경되어도, 이미 그 전에 실행된 함수가 가지고 있는 state는 변경되지 않고, 리렌더링이 발동한다.
https://ko.react.dev/reference/react/useState#updating-state-based-on-the-previous-state
그러나 업데이터 함수를 이용하면 대기중인 state를 가져와서 다음 state를 계산할 수 있다.
따라서, 화살표 함수를 이용해 업데이트하면 prevIndex 가 호출된 시점의 최신 상태 값을 보장하는 것이다.
CSS 정리
overflow-hidden
viewport로 설정한 div 내부의 다른 요소들이 viewport보다 크기가 커도 바깥에 보이지 않게 한다.
transition-transform duration-300 ease-in-out
천천히-빠르게-천천히 애니메이션을 줌
style={{ transform: translateX(-${currentIndex * 100}%) }}
tailwind는 빌드될 때 정적으로 스타일이 지정되기 때문에, 변수에 따라 내용을 스타일값을 변화시키려면 style={{}}과 같은 인라인 스타일 혹은 CSS-in-JS 스타일을 사용해야한다.
tailwind 처음 쓸 때 이걸 몰라서 많이 고생했다...
shrink-0
해당 요소가 flex에 의해 크기가 줄어들지 않고 원본 요소 크기를 유지하게 해준다.
반응형 구현
h-[300px] md:h-[400px] lg:h-[500px]
tailwind의 sm:~ md:~를 활용해서 모바일 반응도 구현했다. tailwind는 모바일 반응형을 구현하기 참 쉬운 것 같다..
'개발 공부 > Next.js' 카테고리의 다른 글
Next, tailwind로 캐러셀 구현하기-2 (무한 루프 슬라이더, 최적화) (0) | 2024.11.26 |
---|