하나의 큰 <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로 변경했다.
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을 사용하지 않았을 때
1. 마우스 이벤트 발생
2. state들 변경(previousX, isDragging), scrollTo 호출
3. 1초에 2번을 몇 백번씩 실행, 이때 화면에 보여지지 않는데 굳이 메모리 값만 변경
requestAnimationFrame을 사용할 때
1. 마우스 이벤트 발생
2. state들 변경(previousX, isDragging), scrollTo 호출
3. 1초에 2번을 몇 백번씩 실행, 이때 화면에 보여지는 최종 값으로 수정
참고자료
- https://developer.mozilla.org/ko/docs/Web/API/window/requestAnimationFrame
- 스크롤 등의 이벤트 최적화하기 + requestAnimationFrame
질문이나 잘못된 점은 댓글로 남겨주세요 :)💖
'React' 카테고리의 다른 글
[react] useRef Hook + 예제 (2) | 2023.04.26 |
---|---|
[React] 자식 컴포넌트에서 부모 컴포넌트로 데이터 전달하는 법 (0) | 2023.04.12 |
[react] react-modal 라이브러리를 이용하여 모달창 구현하기 (0) | 2023.03.15 |
[React] 이미지 슬라이드(가로형,버튼O,반응형) 만들기 (2) | 2023.03.07 |
[React+TS] textarea 줄바꿈하는 법, 입력한 값만큼 height 늘어나게 하는 법 (0) | 2023.02.27 |
댓글