본문 바로가기
React

[react] 웹에서 터치 스크롤 직접 구현하기

by 1two13 2023. 3. 20.
728x90
반응형

하나의 큰 <div> 안에 여러개의 이미지들이 있고, 이 이미지들을 트랙패드를 사용하는 것처럼 터치 스크롤을 구현하고자 했다. 터치 스크롤은 이미지 위에 마우스를 올리고 좌우로 스크롤 했을 때 이동한 위치에 있는 이미지를 보여주는 기능이다.

터치 스크롤 기능 미리보기

코드로는 어떻게 구현할 수 있을까?

1. 먼저 여러개의 이미지들을 담고 있는 가장 큰 <div>에게 3개의 이벤트를 줄거다. onMouseDown, onMouseMove, onMouseUp 이벤트이다. onMouseDown은 마우스 왼쪽 버튼을 클릭했을 때, onMouseMove는 마우스를 움직였을 때, onMouseUp은 <div> 영역을 벗어났을 때 호출된다. 그리고 div에게 ref 속성을 주어 특정 DOM 즉, div를 선택할 수 있게 해줬다. 

이렇게 구구절절 설명한 것은 아래 코드를 보면 된다.

 // return 코드
 <div
   ref={ref}
   onMouseDown={handleMouseDown}
   onMouseMove={handleMouseMove}
   onMouseUp={handleMouseUp}
   className="inline-block align-middle w-[94vw] whitespace-nowrap overflow-x-auto"
 >
  // 이미지들
 </div>

 

2. 그리고 관리될 state들과 ref를 미리 정의했다.

  const ref = useRef<HTMLDivElement>(null);
  const div = ref.current;
  const refId = useRef<number | null>(null);
  const [isDragging, setIsDragging] = useState(false);
  const [previousX, setPreviousX] = useState(0);

 

3. onMouseDown, onMouseUp 이벤트의 코드를 작성했다.

  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    setIsDragging(true);
    setPreviousX(e.clientX);
  };

  const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
    setIsDragging(false);
  };

왼쪽 마우스를 클릭했을 때 state 변경 함수를 사용하여 isDragging 값을 true로 변경하고, 현재 위치(e.clientX)를 setPrevious 함수에 저장했다. 참고로 e.clientX는 이벤트가 발생한 viewport 내의 수평 좌표를 제공한다. 

 

<div> 영역을 벗어났을 때는 state 변경 함수를 사용하여 isDragging 값을 false로 변경했다.

 

728x90

4. 마지막으로 onMouseMove 이벤트의 코드를 작성했다.

드래깅되었거나, <div>가 클릭되었거나, refId.current 값이 있다면 return 해버리고, 

그렇지 않다면 refId.current에 값을 할당해줬다.

  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!isDragging || !div || refId.current) {
      return;
    }

    refId.current = requestAnimationFrame(() => {
      if (div) {
        const delta = e.clientX - previousX;
        div.scrollLeft += delta;
        setPreviousX(e.clientX);
      }
      refId.current = null;
      // 아래 예제에서 같이 사용될 코드(지금은 몰라도 무관합니다.)
      tickEvent.current.tickCnt += 1;
    });
  };

 

window.requestAnimationFrame(callback)

브라우저에게 수행하기를 원하는 애니메이션을 알리고, 다음 re-paint가 진행되기 전에 해당 애니메이션을 업데이트하는 함수를 호출한다. 인자로는 re-paint 이전에 실행할 콜백을 받는다. 

 

즉, requestAnimationFrame을 사용해 re-paint 이전에 해당 애니메이션을 업데이트하는 함수를 호출하고 이 값을 refId.current 값에 담아준 것이다. 

 

requestAnimationFrame은 화면에 새로운 애니메이션을 업데이트할 준비가 될 때마다 호출하는 것이 좋다.(state가 아닌, 화면을 변경할 때 사용) 콜백의 수는 보통 1초에 60회지만, 일반적으로 대부분의 브라우저에서는 W3C 권장사항에 따라 콜백의 수가 디스플레이 주사율과 일치한다.

 

requestAnimationFrame은 브라우저가 렌더링할 수 있는 능력보다 함수가 실행되는 횟수가 더 많은 문제를 해결하기 위해 사용한다. 다시말해, requestAnimationFrame은 불필요한 콜스택이 호출되지 않도록 해준다.

여기서 불필요한 콜스택이란 화면에 보여지는 값이 아닌데 굳이 메모리 값을 변경하는 것을 말한다. 

 

반응형

코드로 직접 실험을 해보면 그 결과를 확인할 수 있다. 아래와 같은 결과를 확인하기 위해서는 코드를 추가해줘야 한다.

  // 추가된 변수
  const tickEvent = useRef<{ start: Date; tickCnt: number }>({ start: new Date(), tickCnt: 0 });
  
  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    setIsDragging(true);
    setPreviousX(e.clientX);
    // 추가된 코드
    tickEvent.current = { start: new Date(), tickCnt: 0 };
  };
  
  const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
    setIsDragging(false);
    // 추가된 코드
    console.log(`${(+new Date() - +tickEvent.current.start) / 1000}초`, tickEvent.current.tickCnt);
  };

requestAnimationFrame사용 전
requestAnimationFrame사용 후

마우스를 흔든 시간동안 함수가 몇 번 호출되었는지 알 수 있도록 콘솔로그를 작성했다. requestAnimationFrame를 사용했을 때 불필요한 콜스택이 호출되지 않기 때문에 함수가 실행되는 횟수가 적은 것을 확인할 수 있다. 

 

위의 내용들을 정리해보자면 이와 같다. 

requestAnimationFrame을 사용하지 않았을 때

1. 마우스 이벤트 발생

2. state들 변경(previousX, isDragging), scrollTo 호출

3. 1초에 2번을 몇 백번씩 실행, 이때 화면에 보여지지 않는데 굳이 메모리 값만 변경

 

requestAnimationFrame을 사용할 때

1. 마우스 이벤트 발생

2. state들 변경(previousX, isDragging), scrollTo 호출

3. 1초에 2번을 몇 백번씩 실행, 이때 화면에 보여지는 최종 값으로 수정

 

 

참고자료


 


질문이나 잘못된 점은 댓글로 남겨주세요 :)💖

728x90
반응형

댓글