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

배지호·2024년 2월 26일

캐러셀

목록 보기
2/2
post-thumbnail

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

설계

이번 편에서는 지난 번에 만들어 놓은 무한 캐러셀에 모바일 유저를 고려한 스와이프 기능을 추가해보려 한다.
유저가 손가락으로 미는 방향으로 이미지가 이동하도록 구현해보겠다.

구현

let touchStartX: number; 
let touchEndX: number; 
  • 터치한 곳의 위치를 저장하는 변수
  const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
    touchStartX = e.nativeEvent.touches[0].clientX;
  };
  • 처음 터치했을 때 호출하는 함수
  • 처음 터치한 곳의 x좌표를 할당
  const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
    const curTouchX = e.nativeEvent.changedTouches[0].clientX;
    if (carouselRef.current !== null) {
      carouselRef.current.style.transition = '';
      carouselRef.current.style.transform = `translateX(calc(-${curIdx + 1}00% - ${
        touchStartX - curTouchX
      }px))`;
    }
  };
  • 터치를 하는 과정에서 호출하는 함수
  • 터치를 한 만큼 캐러셀 박스를 움직여서 다음 이미지로 이동한다.
const handleTouchEnd = (e: React.TouchEvent<HTMLDivElement>) => {
    touchEndX = e.nativeEvent.changedTouches[0].clientX;
    const moveToNext = touchStartX - touchEndX > 50;
    const moveToPrev = touchEndX - touchStartX > 50;
    if (moveToNext) {
      handleClick(1);
    } else if (moveToPrev) {
      handleClick(-1);
    } else {
      if (carouselRef.current !== null) {
        carouselRef.current.style.transition = 'all 0.5s ease-in-out';
        carouselRef.current.style.transform = `translateX(-${curIdx + 1}00%)`;
      }
    }
  };
  • 터치가 끝났을 때 호출하는 함수
  • 터치의 끝점과 시작점의 차이를 계산하여 50의 차이가 있으면 넘기도록 설정
  • 50을 넘지 못하면 애니메이션 효과와 함께 원래 위치로 이동

최종

import styled from 'styled-components';
import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io';
import { useEffect, useRef, useState } from 'react';

const Container = styled.div`
  position: relative;
  width: 100%;
  height: 38rem;
  @media ${({ theme }) => theme.device.mobile} {
    height: 26rem;
  }
  overflow-x: clip;
`;

const CarouselBox = styled.div`
  display: flex;
  width: 100%;
  height: 100%;
`;

const Img = styled.img`
  min-width: 100%;
  width: 100%;
  height: 100%;
`;

interface Button {
  $isLeft: boolean;
}

const Button = styled.button<Button>`
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  ${({ $isLeft }) => ($isLeft ? 'left: 0;' : 'right: 0;')}
  z-index: 1;
`;

interface Banner {
  id: string;
  imageUrl: string;
  linkUrl: string;
}

interface Carousel {
  carouselList: Array<Banner>;
}

const Carousel = ({ carouselList }: Carousel) => {
  const carouselRef = useRef<HTMLDivElement>(null);

  const [curIdx, setCurIdx] = useState(0);

  const carouselArray = [
    carouselList[carouselList.length - 1],
    ...carouselList,
    carouselList[0],
  ];

  const fakeMove = (index: number) => {
    setTimeout(() => {
      setCurIdx(index);
      if (carouselRef.current !== null) {
        carouselRef.current.style.transition = '';
      }
    }, 500);
  };

  const handleClick = (shift: number) => {
    const nextIdx = curIdx + shift;
    if (nextIdx === carouselList.length) {
      fakeMove(0);
    } else if (nextIdx === -1) {
      fakeMove(carouselList.length - 1);
    }
    setCurIdx(nextIdx);
    if (carouselRef.current !== null) {
      carouselRef.current.style.transition = 'all 0.5s ease-in-out';
    }
  };

  let touchStartX: number;
  let touchEndX: number;

  const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
    touchStartX = e.nativeEvent.touches[0].clientX;
  };

  const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
    const curTouchX = e.nativeEvent.changedTouches[0].clientX;
    if (carouselRef.current !== null) {
      carouselRef.current.style.transition = '';
      carouselRef.current.style.transform = `translateX(calc(-${curIdx + 1}00% - ${
        touchStartX - curTouchX
      }px))`;
    }
  };

  const handleTouchEnd = (e: React.TouchEvent<HTMLDivElement>) => {
    touchEndX = e.nativeEvent.changedTouches[0].clientX;
    const moveToNext = touchStartX - touchEndX > 50;
    const moveToPrev = touchEndX - touchStartX > 50;
    if (moveToNext) {
      handleClick(1);
    } else if (moveToPrev) {
      handleClick(-1);
    } else {
      if (carouselRef.current !== null) {
        carouselRef.current.style.transition = 'all 0.5s ease-in-out';
        carouselRef.current.style.transform = `translateX(-${curIdx + 1}00%)`;
      }
    }
  };

  useEffect(() => {
    if (carouselRef.current !== null) {
      carouselRef.current.style.transform = `translateX(-${curIdx + 1}00%)`;
    }
  }, [curIdx]);

  return (
    <Container>
      <Button onClick={() => handleClick(-1)} $isLeft={true}>
        <IoIosArrowBack size={36} />
      </Button>
      <CarouselBox
        ref={carouselRef}
        onTouchStart={handleTouchStart}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
      >
        {carouselArray.map(
          (val, idx) => val && <Img key={idx} src={val.imageUrl} />,
        )}
      </CarouselBox>
      <Button onClick={() => handleClick(1)} $isLeft={false}>
        <IoIosArrowForward size={36} />
      </Button>
    </Container>
  );
};

export default Carousel;

Reference

React Infinite Carousel 만들기

profile
파워 벨로거가 될 남자

0개의 댓글