초보 개발자의 성장 일기

[React] 캐로셀 직접 구현하기 본문

Development/React JS

[React] 캐로셀 직접 구현하기

YUNA 2023. 12. 15. 22:32

 

캐로셀이라는 의미를 안지는 사실 얼마 되지 않았다.

 

에버랜드에서 오랫동안 아르바이트를 했는데 마침 일을 했던 곳이 회전목마랑 같은 구역이었다.

에버랜드에서는 회전목마를 로얄쥬빌리캐로셀이라고 불려서

회전목마인가.. 했는데 그 뜻이 맞았다.

회전목마처럼 데이터를 옆으로 보여주는것을 캐로셀이라고 부르는 것이었다.

 

라이브러리를 사용하면 간편하지만,

사용하기전에 직접 구현을 해서 어떻게 코드를 짜야하는지 아는게 더 중요한것 같다.

 

이전에 jquery를 사용해서 이미지 자동 슬라이드 구현을 해 본적이있었다.

이미지를 보여줄 틀을 잡아놓고

그 안에 이미지를 flex로 나열 후

틀을 넘어간 이미지들은 overflow - hidden으로 가려주고

이미지 가로 크기만큼 -마진값을 넣어주어 구현했었다.

 

리액트로도 비슷하게 구현이 가능할것같다.

그리고 사용자는 옆으로 스와이프로 이미지를 넘기니까,

그 로직만 넣으면 충분히 구현이 쉬울 것이라는 생각이 들었다.

 

찾아보니, margin보다 transition속성을 주면 더 스무스하게 이미지가 넘어간다고 한다.

 

스와이프를 어떻게 구현하지 찾아보고 생각을 많이 해봤는데 마우스 클릭시점과 마우스를 뗀 시점을 구하는것으로 좁혀졌다.

그래서 드래그 관련 기능에 대해서 많이 찾아보고 공부하였다.

 

컴퓨터와 모바일의 터치가 달라, 두 기기를 구분해서 작성하였다.

 

// 데스크탑 드래그
const [mouseDownClientX, setMouseDownClientX] = useState(0);
const [mouseDownClientY, setMouseDownClientY] = useState(0);
const [mouseUpClientX, setMouseUpClientX] = useState(0);
const [mouseUpClientY, setMouseUpClientY] = useState(0);

// 모바일 드래그
const [touchedX, setTouchedX] = useState(0);
const [touchedY, setTouchedY] = useState(0);

x, y좌표를 각각 데스크탑이랑 모바일로 나눠서 구현을 해 주었고

데스크탑은 마우스, 모바일은 터치로 지칭했다.

 

const [transformValue, setTransformValue] = useState(0);

이동값도 따로 상태를 만들어 지정해주었다.

 

const [currentPage, setCurrentPage] = useState(0);

현재페이지도 상태를 만들어주었고 이 현재페이지는 추후에 현재 페이지를 나타내는 아이콘에 같이 사용할 예정이다.

 

  // 다음 슬라이드로 이동
  const handleNextSlide = useCallback(() => {
    if (transformValue === -327 * (images.length - 1)) {
      return;
    }
    setTransformValue((prevTransform) => {
      const nextPage = currentPage + 1;
      setCurrentPage(nextPage !== currentPage ? nextPage : currentPage);
      return prevTransform - 327;
    });
  }, [currentPage, transformValue, images.length]);

  // 이전 슬라이드로 이동
  const handlePrevSlide = useCallback(() => {
    if (transformValue === 0) {
      return;
    }
    setTransformValue((prevTransform) => {
      const prevPage = currentPage - 1;
      setCurrentPage(prevPage !== currentPage ? prevPage : currentPage);
      return prevTransform + 327;
    });
  }, [currentPage, transformValue]);

다음으로 이동하는 슬라이드와 이전으로 이동하는 슬라이드를 구현했는데

useCallback 훅을 사용하여 함수를 메모이제이션해서 함수가 새로 생성되는 것을 방지하고 성능을 최적했다.

 

다음페이지로 넘길때 마지막 페이지일 경우에는 이동이 되지 않도록 return 값을 넣어 주었다.

이미지의 width값이 327px이어서 이동값이 -327 * 이미지 길이에서 1을 뺀값이랑 같을 경우 즉, 마지막 페이지일 경우 다음페이지도 이동하지 않는다.

그리고 이동값은 다음 페이지는 현재 페이지의 +1을 해서 변수로 선언 후에

setCurrentPage를 사용하여 현재 페이지를 업데이트한다. 현재 페이지와 다음 페이지가 같지 않을 때만 업데이트가 된다.

 

  // 데스크탑 드래그 시작 시 마우스의 x, y 좌표 저장
  const onMouseDown = useCallback((e) => {
    setMouseDownClientX(e.clientX);
    setMouseDownClientY(e.clientY);
  }, []);

  // 데스크탑 드래그 종료 시 마우스의 x, y 좌표 저장
  const onMouseUp = useCallback((e) => {
    setMouseUpClientX(e.clientX);
    setMouseUpClientY(e.clientY);
  }, []);

데스크탑 드래스 시작과 종료시에 x, z좌표를 저장해주었다.

 

// 드래그에 의한 슬라이드 변경 감지
  useEffect(() => {
    const dragSpaceX = Math.abs(mouseDownClientX - mouseUpClientX);
    const dragSpaceY = Math.abs(mouseDownClientY - mouseUpClientY);
    const vector = dragSpaceX / dragSpaceY;

    if (mouseDownClientX !== 0 && dragSpaceX > 100 && vector > 2) {
      if (mouseUpClientX < mouseDownClientX) {
        handleNextSlide();
      }
      if (mouseUpClientX > mouseDownClientX) {
        handlePrevSlide();
      }
    }
  }, [mouseDownClientX, mouseUpClientX, mouseDownClientY, mouseUpClientY, handleNextSlide, handlePrevSlide]);

드래그에 의한 슬라이드 변경 감지를 절대값을 사용해서 dragSpaceX 및 dragSpaceY는 마우스 다운 이벤트와 마우스 업 이벤트 간의 X 및 Y 좌표 차이를 나타낸다. 음수값이 아닌 양수값으로 변환된다. vector는 드래그 공간의 X 좌표 차이를 Y 좌표 차이로 나눈 값을 나타낸다.

아래의 조건을 확인하고 조건 만족시 슬라이드를 구현한다.

  • mouseDownClientX !== 0: 마우스 다운 이벤트가 발생했는지 확인합니다.
  • dragSpaceX > 100: X 좌표의 변화가 일정 이상이어야 드래그로 간주합니다.
  • vector > 2: 드래그의 방향이 주로 가로 방향이어야 합니다.

마우스 업 이벤트의 X 좌표에 따라서 다음 슬라이드로 이동할지, 이전 슬라이드로 이동할지 결정한다.

  • mouseUpClientX < mouseDownClientX: 마우스 업 이벤트의 X 좌표가 마우스 다운 이벤트의 X 좌표보다 작으면 다음 슬라이드로 이동하는 함수인 handleNextSlide가 호출된다.
  • mouseUpClientX > mouseDownClientX: 마우스 업 이벤트의 X 좌표가 마우스 다운 이벤트의 X 좌표보다 크면 이전 슬라이드로 이동하는 함수인 handlePrevSlide가 호출된다.

 

모바일 터치로 비슷하게 구현했다.

  // 모바일 터치 시작 시 좌표 저장
  function onTouchStart(e) {
    setTouchedX(e.changedTouches[0].pageX);
    setTouchedY(e.changedTouches[0].pageY);
  }

비슷하게 터치 시작시 좌표를 저장하였다.

 

  // 모바일 터치 종료 시 드래그 거리를 계산하여 슬라이드 변경 감지
  function onTouchEnd(e) {
    const distanceX = touchedX - e.changedTouches[0].pageX;
    const distanceY = touchedY - e.changedTouches[0].pageY;
    const vector = Math.abs(distanceX / distanceY);

    if (distanceX > 30 && vector > 2) {
      handleNextSlide();
    } else if (distanceX < -30 && vector > 2) {
      handlePrevSlide();
    }
  }

데스크탑과 마찬가지로 거리를 계산해서 슬라이드 변경을 감지했다.

 

<div className="absolute bottom-[14px] flex justify-center inset-x-0">
{Array.from({ length: images.length }, (_, index) => (
  <IoEllipseSharp
    key={index}
    size="8"
    color={currentPage === index ? '#589BF7' : '#ffffff'}
    className="inline mx-1"
  />
))}
</div>

아래 현재 위치를 확인하기 위해서 현재 페이지 상태를 확인하고 인덱스와 같으면 색을 넣어주고 아닐 경우는 하얀색을 넣어주어서 구현을 완성했다.

 

#엘리스트랙 #엘리스트랙후기 #리액트네이티브강좌 #온라인코딩부트캠프 #온라인코딩학원 #프론트엔드학원 #개발자국비지원 #개발자부트캠프 #국비지원부트캠프 #프론트엔드국비지원 #React #Styledcomponent #React Router Dom #Redux #Typescript #Javascript