저번에 구현했던 캐러셀은 마지막 사진에서 맨 앞 사진으로 돌아올 때 애니메이션이 어색하다는 단점이 있었다. 무한 루프 슬라이더를 구현해보자.
어떻게 구현하면 좋을까?
이미지 배열의 맨 앞과 끝에 가장 마지막 이미지와 제일 첫 이미지를 붙이고, 끝 이미지 일 경우 속도를 0으로 바꿔서 이동시킨다.
"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/2.jpg" },
];
const Carousel = () => {
const [currentIndex, setCurrentIndex] = useState(1);
const [isTransitioning, setIsTransitioning] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const extendedImages = [images[images.length - 1], ...images, images[0]]; //이미지 배열 붙임
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 = () => {
if (!isTransitioning) { //isTransitioning 아닐때만
setIsTransitioning(true);
setCurrentIndex((prevIndex) => prevIndex + 1);
}
};
const slidePrev = () => {
if (!isTransitioning) {
setIsTransitioning(true);
setCurrentIndex((prevIndex) => prevIndex - 1);
}
};
const handleTransitionEnd = () => { //isTransitioning 끝난 후, 맨 마지막이거나 처음일 경우 이동
setIsTransitioning(false);
if (currentIndex === 0) {
setCurrentIndex(extendedImages.length - 2);
} else if (currentIndex === extendedImages.length - 1) {
setCurrentIndex(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 ease-in-out h-full w-full"
style={{
transform: `translateX(-${currentIndex * 100}%)`,
transitionDuration: `${isTransitioning ? 300 : 0}ms`, //속도 변경
}}
onTransitionEnd={handleTransitionEnd}
>
{extendedImages.map((image, index) => (
<div key={index} className="w-full shrink-0 relative">
<Image
src={image.src}
alt={`Slide ${index}`}
className="object-cover"
width={1800}
height={700}
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) => (
<button
key={index}
className={`h-2 rounded-full duration-300 ease-in-out ${
index === (currentIndex - 1 + images.length) % images.length
? "w-6 bg-black"
: "w-2 bg-gray-300"
}`}
onClick={() => {
setIsTransitioning(true);
setCurrentIndex(index + 1);
}}
/>
))}
</div>
</div>
);
};
export default Carousel;
최적화 시도해보기
아직 아쉬운 부분이 많다..
렌더링 될 때 마다 extendedImage를 새로 계산해줘야할 것이고, slideNext와 slidePrev가 새롭게 생성될 것이다.
extendedImage는 useMemo로, slideNext와 slidePrev는 useCallback으로 최적화를 해보자.
"use client";
import { useState, useEffect, useMemo, useCallback } from "react";
import Image from "next/image";
const images = [
{ src: "/image/sample/slide 1.png", priority: true },
{ src: "/image/sample/slide 2.png", priority: false },
{ src: "/image/sample/slide 3.png", priority: false },
{ src: "/image/sample/slide 4.png", priority: false },
{ src: "/image/sample/slide 5.png", priority: false },
];
const Carousel = () => {
const [currentIndex, setCurrentIndex] = useState(1);
const [isTransitioning, setIsTransitioning] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const extendedImages = useMemo(
() => [images[images.length - 1], ...images, images[0]],
[]
);
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 = useCallback(() => {
if (!isTransitioning) {
setIsTransitioning(true);
setCurrentIndex((prevIndex) => prevIndex + 1);
}
}, []);
const slidePrev = useCallback(() => {
if (!isTransitioning) {
setIsTransitioning(true);
setCurrentIndex((prevIndex) => prevIndex - 1);
}
}, []);
const handleTransitionEnd = () => {
setIsTransitioning(false);
if (currentIndex === 0) {
setCurrentIndex(extendedImages.length - 2);
} else if (currentIndex === extendedImages.length - 1) {
setCurrentIndex(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-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] overflow-hidden rounded-3xl"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className="flex transition-transform ease-in-out h-full w-full"
style={{
transform: `translateX(-${currentIndex * 100}%)`,
transitionDuration: `${isTransitioning ? 300 : 0}ms`,
}}
onTransitionEnd={handleTransitionEnd}
>
{extendedImages.map((image, index) => (
<div
key={index}
className="w-full shrink-0 relative hover:scale-110 duration-300"
>
<Image
src={image.src}
alt={`Slide ${index}`}
className="object-cover h-full"
width={1800}
height={500}
priority={image.priority}
/>
</div>
))}
</div>
<button
onClick={slidePrev}
className={`left-4 ${slideButtonStyle} ${
isHovered ? "opacity-100" : "opacity-0"
}`}
aria-label="prev slide button"
>
❮
</button>
<button
onClick={slideNext}
className={`right-4 ${slideButtonStyle} ${
isHovered ? "opacity-100" : "opacity-0"
}`}
aria-label="next slide button"
>
❯
</button>
</div>
<div className="absolute mt-6 left-1/2 transform -translate-x-1/2 flex">
{images.map((_, index) => (
<button
key={index}
onClick={() => {
setIsTransitioning(true);
setCurrentIndex(index + 1);
}}
className="p-2 m-2"
aria-label={`slide ${index} button`}
>
<div
className={`h-2 rounded-full duration-300 ease-in-out ${
index === (currentIndex - 1 + images.length) % images.length
? "w-6 bg-black"
: "w-2 bg-gray-300"
}`}
/>
</button>
))}
</div>
</div>
);
};
export default Carousel;
바뀐 부분 따로 보기
const extendedImages = useMemo(
() => [images[images.length - 1], ...images, images[0]],
[]
);
const slideNext = useCallback(() => {
if (!isTransitioning) {
setIsTransitioning(true);
setCurrentIndex((prevIndex) => prevIndex + 1);
}
}, []);
const slidePrev = useCallback(() => {
if (!isTransitioning) {
setIsTransitioning(true);
setCurrentIndex((prevIndex) => prevIndex - 1);
}
}, []);
'개발 공부 > Next.js' 카테고리의 다른 글
Next, tailwind로 Carousel 구현하기 - 1 (자동 슬라이드) (1) | 2024.09.10 |
---|