개발 공부 기록

나는 무엇을 하는가?

개발 공부/Next.js

Next, tailwind로 캐러셀 구현하기-2 (무한 루프 슬라이더, 최적화)

진!!!!! 2024. 11. 26. 19:00

 

저번에 구현했던 캐러셀은 마지막 사진에서 맨 앞 사진으로 돌아올 때 애니메이션이 어색하다는 단점이 있었다. 무한 루프 슬라이더를 구현해보자.

 

 

 

어떻게 구현하면 좋을까?

이미지 배열의 맨 앞과 끝에 가장 마지막 이미지와 제일 첫 이미지를 붙이고, 끝 이미지 일 경우 속도를 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"
          }`}
        >
          &#10094;
        </button>
        <button
          onClick={slideNext}
          className={`right-4 ${slideButtonStyle} ${
            isHovered ? "opacity-100" : "opacity-0"
          }`}
        >
          &#10095;
        </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"
        >
          &#10094;
        </button>
        <button
          onClick={slideNext}
          className={`right-4 ${slideButtonStyle} ${
            isHovered ? "opacity-100" : "opacity-0"
          }`}
          aria-label="next slide button"
        >
          &#10095;
        </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);
    }
  }, []);