개발_자 2024. 11. 7. 21:24

캐러셀 구현

리뷰 데이터를 기반으로 한 부드럽고 자연스러운 무한 캐러셀을 직접 구현해보려 합니다! 이번 글에서는 라이브러리를 사용하지 않고 자동 이동, 무한 순환 기능을 가진 캐러셀을 만드는 방법을 다뤄볼 것입니다

 

요구사항

이 캐러셀은 사용자에게 리뷰 데이터를 기반으로 부드럽게 리뷰를 확인할 수 있어야 합니다.

주요 요구 사항은 다음과 같습니다.

  • 자동 이동: 일정 시간이 지나면 자동으로 다음 리뷰 이미지가 표시됩니다.
  • 무한 순환: 마지막 이미지에 도달하면 처음 이미지로 자연스럽게 돌아가는 무한 캐러셀을 구현해야 합니다.

이번 구현의 목표는 라이브러리를 사용하지 않고, 무한 캐러셀의 원리를 이해하며 직접 구현하는 것입니다.

 

다른 구현 방법 탐색

  1. 수동 버튼 이동 : 좌우 버튼을 사용하여 사용자가 직접 이미지를 넘기는 방식입니다. 클릭 시 setState로 상태를 업데이트하여 화면을 변경합니다.
  2. 자동 이동 : 이미지가 일정 시간 간격으로 자동으로 다음으로 넘어가는 방식입니다. 일반적으로 setInterval을 사용하여 구현하며, 끝에 도달하면 슬라이드쇼처럼 멈추거나 처음부터 다시 재생할 수 있습니다.
  3. 무한 스크롤: 마지막 이미지에서 자연스럽게 처음 이미지로 이어지게 하는 방식입니다. 배열을 확장하여 반복되는 요소를 추가하거나, CSS translateX를 조정하여 무한히 순환합니다.

(좌) 무한 스크롤 (우) 수동 버튼이동

 

 

- 참고한 blog

 

[React] 무한 캐러셀(Infinite Carousel) 구현하기

https://velog.io/@gyuri092/React-무한-캐러셀Infinite-Carousel-구현하기

 

[React] 무한 캐러셀(Infinite Carousel) 구현하기

라이브러리 없이 직접 무한 캐러셀을 구현해보자!

velog.io

[Next.js] 무한 캐러셀 컴포넌트 직접 구현하기 (feat. Tailwind CSS)

https://jjang-j.tistory.com/157

 

[Next.js] 무한 캐러셀 컴포넌트 직접 구현하기 (feat. Tailwind CSS)

시작하며...지난 기초, 중급 프로젝트에서 캐러셀을 라이브러리를 사용하여 구현하였다. 그때 회고를 보면 나중에는 라이브러리가 아닌 직접 구현해보고 싶다고 글을 적었었다.그런데 마침 이

jjang-j.tistory.com

Next.js에서 Carousel 컴포넌트를 구현해보자 (Web)

https://velog.io/@turtlemana/Carousel-컴포넌트를-구현해보자-Web

 

Next.js에서 Carousel 컴포넌트를 구현해보자 (Web)

Carousel 컴포넌트는 사용자에게 보여주고 싶은 정보들을 보기 편한 UI로 가독성있게 전달하고자 할 때 자주 활용되는 컴포넌트이다.

velog.io

[React] 라이브러리 없이 캐러셀 만들기 (1/2)

https://velog.io/@blueapple99/React-라이브러리-없이-캐러셀-만들기

 

[React] 라이브러리 없이 캐러셀 만들기 (1/2)

라이브러리 없이 무한 캐러셀 만들기 (1/2)

velog.io

 

React와 프레임워크에서의 캐러셀 구현 방식

  • React: useState와 useEffect로 상태 관리, setInterval을 통해 인덱스 주기적으로 업데이트할 수 있습니다.
  • CSS 애니메이션: @keyframes 와 translateX 속성을 사용해 CSS만으로 애니메이션을 구성할 수 있습니다.
  • JS기반 라이브러리: Swiper.js나 Slick 같은 라이브러리를 사용해 다양한 효과와 옵션을 간편히 적용할 수 있습니다.

 

내가 선택한 무한 캐러셀 구현 방식

  • useState로 현재 인덱스를 관리하고, useEffect에서 setInterval로 인덱스를 자동 증가시켜 무한 스크롤 효과 구현합니다.
  • 자연스러운 무한 캐러셀을 위해 배열을 3배 확장한 extendedReviewArr 배열을 사용해, 캐러셀 끝에서 첫 번째 이미지로 돌아갈 때 부드럽게 이어지도록 구현합니다.
  • 마지막 인덱스에 도달하면 setTimeout으로 인덱스를 0으로 리셋해 부드럽게 처음 이미지로 돌아도록 구현합니다.

 

구현 후 개선점 발견 및 코드 수정

  1. 배열 확장: 초기에는 리뷰 데이터를 한 번만 불러와 캐러셀을 만들었지만, 모바일뿐만 아니라 웹 화면에서도 무한히 순환하는 느낌을 주기 위해 배열을 extendedReviewArr로 3배 확장했습니다. 이를 통해 마지막 이미지에서 첫 이미지로 자연스럽게 이어지는 효과 강화했습니다.
  2. 첫 번째 인덱스에서의 애니메이션 제거: getCarouselStyle 함수에서 transition 속성을 조정하여, 첫 번째 인덱스로 돌아갈 때 애니메이션(되감기) 제거했습니다. 이를 통해 마지막 이미지에서 첫 번째 이미지로 돌아갈 때 끊김 없이 자연스럽게 이어지는 무한 캐러셀 효과 구현합니다.

 

개선 전 코드

// 초기 구현
const [currentIndex, setCurrentIndex] = useState(0);
const reviewArr = [...reviewsData];  // 원본 배열

useEffect(() => {
  const interval = setInterval(() => {
    setCurrentIndex((prevIndex) => (prevIndex + 1) % reviewArr.length);
  }, 3000);

  return () => clearInterval(interval);
}, []);

이 코드에서는 기본적인 자동 슬라이딩이 구현되어 있지만, 끝에 도달하면 부드럽게 처음 이미지로 돌아가는 효과가 부족했습니다.

 

개선 코드

// 초기 구현
const [currentIndex, setCurrentIndex] = useState(0);
const extendedReviewArr = [...reviewsData, ...reviewsData, ...reviewsData];  // 배열 확장

  useEffect(() => {
    if (extendedReviewArr.length > 0) {
      const interval = setInterval(() => {
        setCurrentIndex((prevIndex) => (prevIndex + 1) % extendedReviewArr.length);
      }, 3000);

      return () => clearInterval(interval);
    }
  }, [extendedReviewArr.length]);

  useEffect(() => {
    if (extendedReviewArr.length > 0 && currentIndex === extendedReviewArr.length - 16) {
      const timeout = setTimeout(() => {
        setCurrentIndex(0); // 첫 번째 이미지로 돌아가기
      }, 500);

      return () => clearTimeout(timeout);
    }
  }, [currentIndex, extendedReviewArr.length]);

  const getCarouselStyle = () => {
    return {
      transform: `translateX(${-currentIndex * (216 + 16)}px)`,
      transition: currentIndex === 0 ? 'none' : 'transform 0.5s ease-in-out', // 첫 번째 이미지로 돌아갈 때는 애니메이션 없음
    };
  };

개선 후 동작

  1. 배열 순환: 3배 확장된 배열에서 16번째 이미지가 끝나면 setTimeout을 통해 부드럽게 첫 번째 이미지로 자연스럽게 넘어갑니다.
  2. 자동 슬라이드 및 부드러운 순환 효과 : setInterval을 사용하여 자동으로 리뷰가 슬라이드되고, currentIndex가 변경됩니다. 마지막 이미지에서 첫 번째 이미지로 돌아가는 과정에서 애니메이션 효과를 제거해, 자연스럽고 부드럽게 이어지도록 설정했습니다.

캐러셀 코드 개선 시 커밋 로그

feat: 리뷰 캐러셀 구현 → fix: 리뷰 캐러셀 화면 비율 조정 → fix: 캐러셀 리뷰 수정

 

전체 코드

'use client';

import { Review } from '@/types/reviewData.types';
import { createClient } from '@/utils/supabase/client';
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import { useEffect, useState } from 'react';

const Carousel = () => {
  const browserClient = createClient();
  const [currentIndex, setCurrentIndex] = useState(0);

  const getReviews = async () => {
    const response = await browserClient
      .from('reviews')
      .select('*')
      .filter('image_url', 'neq', '[]')
      .order('created_at', { ascending: false })
      .limit(8);

    if (response.error) {
      console.error(response.error);
    }

    if (response.data === null) {
      return [];
    }

    return response.data;
  };

  const { data: reviewsData = [], isLoading } = useQuery({
    queryKey: ['reviews'],
    queryFn: getReviews,
  });

  const extendedReviewArr: Review[] = [...reviewsData, ...reviewsData, ...reviewsData];

  useEffect(() => {
    const interval = setInterval(() => {
      setCurrentIndex((prevIndex) => (prevIndex + 1) % extendedReviewArr.length);
    }, 3000);

    return () => clearInterval(interval);
  }, [extendedReviewArr.length]);

  useEffect(() => {
    if (currentIndex === extendedReviewArr.length - 16) {
      const timeout = setTimeout(() => {
        setCurrentIndex(0);
      }, 500);

      return () => clearTimeout(timeout);
    }
  }, [currentIndex, extendedReviewArr.length]);

  const getCarouselStyle = () => {
    return {
      transform: `translateX(${-currentIndex * (216 + 16)}px)`,
      transition: currentIndex === 0 ? 'none' : 'transform 0.5s ease-in-out',
    };
  };

  if (isLoading) return <div>Loading...</div>;

  return (
    <div className='overflow-hidden w-full'>
      <div
        className='flex justify-center'
        style={getCarouselStyle()}
      >
        {extendedReviewArr.map((review, index) => {
          const imgUrls = review.image_url as string[] | null;
          return (
            <div
              key={`${review.id}-${index}`}
              className='mx-[8px]'
            >
              <div className='relative w-[216px] h-[222px] rounded-t-lg overflow-hidden'>
                {imgUrls && imgUrls.length > 0 && (
                  <Image
                    src={imgUrls[0]}
                    alt='리뷰 이미지'
                    layout='fill'
                    objectFit='cover'
                    priority
                  />
                )}
              </div>
              <div className='w-[216px] h-[104px] border-2 rounded-b-lg'>
                <p>{review.user_name}</p>
                <p>{review.content}</p>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default Carousel;

 

이처럼 React로 무한 캐러셀을 구현하는 과정에서 배열 확장 및 애니메이션 효과를 활용해 더 자연스러운 사용자 경험을 제공할 수 있습니다. 이번 예시가 무한 캐러셀의 원리를 이해하고 직접 구현하는 데에 도움이 되길 바랍니다.!