본문 바로가기

React 배너 슬라이더 만들기 + touch evnet 본문

React

React 배너 슬라이더 만들기 + touch evnet

개발자로 거듭나기 2024. 1. 4. 11:10
반응형

React 배너 슬라이더 만들기 + touch evnet

기능

  • 설정한 INTERVAL 마다 다음 화면으로 넘어감
  • touch event를 구현하여 오른쪽이나 왼쪽으로 밀 때 마다, 화면이 넘어감

코드

import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';

const dummyBannerList = ['tomato', 'violet', 'skyblue', 'teal', 'orange'];
const INVERVAL = 2500;

const Banner = () => {
  const divRef = useRef<HTMLDivElement>(null);

  const [startX, setStartX] = useState(0);
  const [offset, setOffset] = useState(0);
  const [width, setWidth] = useState<number>(0);
  const [currentIndex, setCurrentIndex] = useState<number>(0);

  /**
   * ['tomato', 'violet', 'skyblue', 'tomato'];
   * currentIndex 0 1 2 0 1 2 0 1 2
   * nextIndex    1 2 3 1 2 3 1 2 3
   * 처음에 current 0 일 때 한번 밀고 violet 보임
   * 두번째 current 1 일 때 두번 밀고 skyblue 보임
   * 세번째 current 2 일 때 세번 밀고 tomato 보임
   * 바로 위의 상황에서 if문에 걸리게되고 currentIndex는 0이 되고 600ms 후에 0번으로 되돌아온다, 순식간에 (transition 0ms)
   * 다음 currentIndex 0이 되어서 한번 밀고 violet 보임 => 반복
   */
  const handleSlider = () => {
    const nextIndex = currentIndex + 1;
    setCurrentIndex(nextIndex);
    divRef.current!.style.transition = 'all 600ms';
    divRef.current!.style.transform = `translateX(${-width * nextIndex}px)`;

    if (nextIndex === dummyBannerList.length - 1) {
      setTimeout(() => {
        divRef.current!.style.transition = 'all 0ms';
        divRef.current!.style.transform = `translateX(-${width}px)`;
      }, 600);
      setCurrentIndex(1);
    }
  };

  // 보이는 index 계산 [1 / 5] 이런식으로
  const calIndex = (index: number) => {
    if (index === dummyBannerList.length - 1) {
      return 1;
    }

    if (index === 0) {
      return dummyBannerList.length - 2;
    }
    return index;
  };

  const handleTouchStart = (e: React.TouchEvent) => {
    setStartX(e.touches[0].clientX);
  };

  const handleTouchMove = (e: React.TouchEvent) => {
    const moveX = e.touches[0].clientX - startX;
    setOffset(moveX);
  };

  // handleSlider와 비슷한 원리
  const handleTouchEnd = (e: React.TouchEvent) => {
    // 오른쪽에서 왼쪽으로 긁기 index plus
    if (offset < 0) {
      let index = (currentIndex + 1);

      divRef.current!.style.transition = 'all 600ms';
      divRef.current!.style.transform = `translateX(${-width * index}px)`;

      if (index === dummyBannerList.length - 1) {
        setTimeout(() => {
          divRef.current!.style.transition = 'all 0ms';
          divRef.current!.style.transform = `translateX(-${width}px)`;
        }, 500);
        index = 1;
      }

      setCurrentIndex(index);
    } else if (offset > 0) { // 왼쪽애서 오른쪽으로 긁기 index minus
      let index = (currentIndex - 1);

      divRef.current!.style.transition = 'all 600ms';
      divRef.current!.style.transform = `translateX(${-width * index}px)`;

      if (index === 0) {
        setTimeout(() => {
          divRef.current!.style.transition = 'all 0ms';
          divRef.current!.style.transform = `translateX(${-width * (dummyBannerList.length - 2)}px)`;
        }, 500);
        index = dummyBannerList.length - 2;
      }

      setCurrentIndex(index);
    }

    setOffset(0);
  };

  useLayoutEffect(() => {
    // 화면의 넓이 계산
    setWidth(window.innerWidth);

    // 배너 배열의 앞뒤로 각각 lastElement, firstElement push
    // [a, b, c] 일경우 [c, a, b, c, a] => 끊이지않고 자연스럽게 넘어가게 하기 위한 작업
    const firstElement = dummyBannerList[0];
    const lastElement = dummyBannerList[dummyBannerList.length - 1];
    dummyBannerList.push(firstElement);
    dummyBannerList.unshift(lastElement);
  }, []);

  useEffect(() => {
    if (width === 0) return undefined;
    let intervalId: NodeJS.Timer | null = null;

    intervalId = setInterval(handleSlider, INVERVAL);

    return () => {
      if (intervalId) {
        clearInterval(intervalId);
      }
    };
  }, [width, currentIndex]);

  return (
    <div
      ref={divRef}
      style={{
        width: 'fit-content',
        height: 105,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        overflow: 'hidden',
        transition: 'all 0ms',
        transform: `translateX(-${width}px)`,
      }}
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
    >
      {dummyBannerList.map((banner, index) => (
        <div
          key={`${banner}${index}`}
          style={{
            width,
            height: 'inherit',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            background: banner,
            position: 'relative',
          }}
        >
          <div>
            {banner}
          </div>

          <div
            style={{
              position: 'absolute',
              padding: `${(7)}px ${(15)}px`,
              background: 'rgba(0,0,0,0.7)',
              color: '#fff',
              bottom: 0,
              right: 0,
              fontSize: 12,
            }}
          >
            {`${calIndex(index)} / ${dummyBannerList.length - 2}`}
          </div>
        </div>
      ))}
    </div>
  );
};

export default Banner;
반응형

'React' 카테고리의 다른 글

useDefferedValue  (1) 2023.10.03
useTransition  (0) 2023.10.03
RTK Query 캐쉬 무효화  (0) 2023.10.03
Comments