노현진's Blog

좌우 드래그 스크롤 구현 방법

좌우 드래그 스크롤 구현 방법에 대해 설명하는 페이지입니다.

Posted
Preview Image
By HyunJinNo

Tags

TypeScript, Next.js

Environment

Next.js v14.2.3

1. 개요

이번 글에서는 React/Next.js에서 마우스 또는 터치로 좌우 드래그 스크롤을 구현하는 방법을 설명하겠습니다. 스크롤 기능을 추가할 HTML 태그 요소로 div를 선택하였습니다.

2. Step 1 - DragScrollType 정의

먼저 다음과 같이 추후 구현할 useDragScroll 커스텀 훅의 반환 타입을 정의합니다.

typescript
1export type DragScrollType = {
2  listRef: RefObject<HTMLDivElement>;
3  onDragStart: (e: MouseEvent<HTMLDivElement>) => void;
4  onDragMove: (e: MouseEvent<HTMLDivElement>) => void;
5  onDragEnd: (e: MouseEvent<HTMLDivElement>) => void;
6  onTouchStart: (e: TouchEvent<HTMLDivElement>) => void;
7  onTouchMove: (e: TouchEvent<HTMLDivElement>) => void;
8  onTouchEnd: (e: TouchEvent<HTMLDivElement>) => void;
9};

각 요소에 대해 설명하자면 다음과 같습니다.

  • listRef
    • 드래그 스크롤 기능을 추가하기 위해선 해당 DOM의 scrollLeft 값을 참조해야 합니다.
    • 해당 DOM의 scrollLeft를 얻기 위해 useRef를 사용하여 DOM에 접근합니다.
  • onDragStart
    • 마우스 드래그가 시작되었을 때 호출되는 이벤트입니다.
  • onDragMove
    • 마우스 드래그가 진행 중일 때 호출되는 이벤트입니다.
  • onDragEnd
    • 마우스 드래그가 종료되었을 때 호출되는 이벤트입니다.
  • onTouchStart
    • 터치 드래그가 시작되었을 때 호출되는 이벤트입니다.
  • onTouchMove
    • 터치 드래그가 진행 중일 때 호출되는 이벤트입니다.
  • onTouchEnd
    • 터치 드래그가 종료되었을 때 호출되는 이벤트입니다.

3. Step 2 - useDragScroll 커스텀 훅 생성

다음과 같이 좌우 드래그 스크롤 기능을 제공하는 useDragScroll.ts 파일을 생성하고 상태를 관리하는 변수를 선언합니다.

typescript
1import { MouseEvent, RefObject, TouchEvent, useRef, useState } from "react";
2
3/* ... */
4
5export default function useDragScroll(): DragScrollType {
6  const listRef = useRef<HTMLDivElement>(null);
7
8  // element를 드래그하고 있는지 여부
9  const [isDragging, setIsDragging] = useState<boolean>(false);
10
11  // 드래그 시작 시점의 스크롤 포지션이 포함된 x축 좌표값
12  const [totalX, setTotalX] = useState<number>(0);
13
14  /* ... */
15}

각 변수에 대해 설명하자면 다음과 같습니다.

  • listRef
    좌우 드래그 스크롤을 적용할 DOM에 접근하기 위한 Ref 객체입니다.
  • [isDragging, setIsDragging]
    드래그하고 있는지 여부를 추적합니다.
  • [totalX, setTotalX]
    드래그 시작 시점의 x축 좌표 값을 추적합니다.

4. Step 3 - 마우스 드래그 이벤트 정의하기

다음과 같이 마우스 드래그 이벤트를 정의합니다.

typescript
1// 마우스 드래그 시작
2const onDragStart = (e: MouseEvent<HTMLDivElement>) => {
3  e.preventDefault();
4  setIsDragging(true);
5
6  const x = e.clientX;
7  if (listRef.current && "scrollLeft" in listRef.current) {
8    setTotalX(x + listRef.current.scrollLeft);
9  }
10};
11
12// 마우스 드래그 동작 중
13const onDragMove = (e: MouseEvent<HTMLDivElement>) => {
14  e.preventDefault();
15  if (!isDragging) {
16    return;
17  }
18
19  const scrollLeft = totalX - e.clientX;
20  if (listRef.current && "scrollLeft" in listRef.current) {
21    // 스크롤 발생
22    listRef.current.scrollLeft = scrollLeft;
23  }
24};
25
26// 마우스 드래그 종료
27const onDragEnd = (e: MouseEvent<HTMLDivElement>) => {
28  e.preventDefault();
29  if (!isDragging) {
30    return;
31  }
32
33  if (!listRef.current) {
34    return;
35  }
36
37  setIsDragging(false);
38};

5. Step 4 - 터치 드래그 이벤트 정의하기

다음과 같이 터치 드래그 이벤트를 정의합니다.

typescript
1// 터치 드래그 시작
2const onTouchStart = (e: TouchEvent<HTMLDivElement>) => {
3  setIsDragging(true);
4
5  const x = e.touches[0].pageX;
6  if (listRef.current && "scrollLeft" in listRef.current) {
7    setTotalX(x + listRef.current.scrollLeft);
8  }
9};
10
11// 터치 드래그 동작 중
12const onTouchMove = (e: TouchEvent<HTMLDivElement>) => {
13  if (!isDragging) {
14    return;
15  }
16
17  const scrollLeft = totalX - e.touches[0].pageX;
18  if (listRef.current && "scrollLeft" in listRef.current) {
19    // 스크롤 발생
20    listRef.current.scrollLeft = scrollLeft;
21  }
22};
23
24// 터치 드래그 종료
25const onTouchEnd = () => {
26  if (!isDragging) {
27    return;
28  }
29
30  if (!listRef.current) {
31    return;
32  }
33
34  setIsDragging(false);
35};

터치 드래그 이벤트의 경우 마우스 드래그 이벤트를 정의할 때 사용한 e.preventDefault()를 호출하지 않습니다. e.preventDefault()를 호출하면 Link나 버튼 등을 터치할 때 정상적으로 동작하지 않게 됩니다.

6. Step 5 - 최종 코드

위에서 구현한 useDragScroll 커스텀 훅의 최종 코드는 다음과 같습니다.

typescript
1// useDragScroll.ts
2
3import { MouseEvent, RefObject, TouchEvent, useRef, useState } from "react";
4
5export type DragScrollType = {
6  listRef: RefObject<HTMLDivElement>;
7  onDragStart: (e: MouseEvent<HTMLDivElement>) => void;
8  onDragMove: (e: MouseEvent<HTMLDivElement>) => void;
9  onDragEnd: (e: MouseEvent<HTMLDivElement>) => void;
10  onTouchStart: (e: TouchEvent<HTMLDivElement>) => void;
11  onTouchMove: (e: TouchEvent<HTMLDivElement>) => void;
12  onTouchEnd: (e: TouchEvent<HTMLDivElement>) => void;
13};
14
15export default function useDragScroll(): DragScrollType {
16  const listRef = useRef<HTMLDivElement>(null);
17
18  // element를 드래그하고 있는지 여부
19  const [isDragging, setIsDragging] = useState<boolean>(false);
20
21  // 드래그 시작 시점의 스크롤 포지션이 포함된 x축 좌표값
22  const [totalX, setTotalX] = useState<number>(0);
23
24  // 마우스 드래그 시작
25  const onDragStart = (e: MouseEvent<HTMLDivElement>) => {
26    e.preventDefault();
27    setIsDragging(true);
28
29    const x = e.clientX;
30    if (listRef.current && "scrollLeft" in listRef.current) {
31      setTotalX(x + listRef.current.scrollLeft);
32    }
33  };
34
35  // 마우스 드래그 동작 중
36  const onDragMove = (e: MouseEvent<HTMLDivElement>) => {
37    e.preventDefault();
38    if (!isDragging) {
39      return;
40    }
41
42    const scrollLeft = totalX - e.clientX;
43    if (listRef.current && "scrollLeft" in listRef.current) {
44      // 스크롤 발생
45      listRef.current.scrollLeft = scrollLeft;
46    }
47  };
48
49  // 마우스 드래그 종료
50  const onDragEnd = (e: MouseEvent<HTMLDivElement>) => {
51    e.preventDefault();
52    if (!isDragging) {
53      return;
54    }
55
56    if (!listRef.current) {
57      return;
58    }
59
60    setIsDragging(false);
61  };
62
63  // 터치 드래그 시작
64  const onTouchStart = (e: TouchEvent<HTMLDivElement>) => {
65    setIsDragging(true);
66
67    const x = e.touches[0].pageX;
68    if (listRef.current && "scrollLeft" in listRef.current) {
69      setTotalX(x + listRef.current.scrollLeft);
70    }
71  };
72
73  // 터치 드래그 동작 중
74  const onTouchMove = (e: TouchEvent<HTMLDivElement>) => {
75    if (!isDragging) {
76      return;
77    }
78
79    const scrollLeft = totalX - e.touches[0].pageX;
80    if (listRef.current && "scrollLeft" in listRef.current) {
81      // 스크롤 발생
82      listRef.current.scrollLeft = scrollLeft;
83    }
84  };
85
86  // 터치 드래그 종료
87  const onTouchEnd = () => {
88    if (!isDragging) {
89      return;
90    }
91
92    if (!listRef.current) {
93      return;
94    }
95
96    setIsDragging(false);
97  };
98
99  return {
100    listRef,
101    onDragStart,
102    onDragMove,
103    onDragEnd,
104    onTouchStart,
105    onTouchMove,
106    onTouchEnd,
107  };
108}

7. Step 6 - 좌우 드래그 스크롤 적용하기

다음과 같이 좌우 드래그 스크롤을 적용할 HTML 태그 요소에 이벤트를 등록합니다.

typescript
1const scrollHook = useDragScroll();
typescript
1<div
2  className="overflow-x-auto"
3  ref={scrollHook.listRef}
4  onMouseDown={scrollHook.onDragStart}
5  onMouseMove={scrollHook.onDragMove}
6  onMouseUp={scrollHook.onDragEnd}
7  onMouseLeave={scrollHook.onDragEnd}
8  onTouchStart={scrollHook.onTouchStart}
9  onTouchMove={scrollHook.onTouchMove}
10  onTouchEnd={scrollHook.onTouchEnd}
11>
12  {children}
13</div>

8. Step 7 - 테스트 결과

좌우 드래그 스크롤을 적용한 테스트 결과는 다음과 같습니다.

9. 참고 자료

© HyunJinNo. Some rights reserved.