반응형
간단한 재정렬 기능을 가진 todolist이다.
- 이동 시 해당 아이템이 마우스 커서를 따라오도록 구현:
draggingItem
상태를 추가하여 현재 드래그 중인 아이템을 추적합니다.- 드래그 중인 아이템의 복사본을 마우스 커서 위치에 표시합니다.
- 이동 시 커서가 e-resize 모양이 되도록 변경:
- 각 todo 아이템에
cursor-e-resize
클래스를 추가했습니다.
- 메뉴 목록을 가로로 나열하고 left-align 정렬:
<ul>
요소에flex flex-wrap gap-2 items-start
클래스를 적용하여 가로 정렬과 왼쪽 정렬을 구현했습니다.
- 추가한 글자 길이에 맞게 아이템의 너비가 fit되도록 변경:
- 각
<li>
요소에inline-block
클래스를 추가하여 내용에 맞게 너비가 조정되도록 했습니다.
- 드래그 중인 아이템의 원래 자리를 비우도록 구현:
- 드래그 시작 시 해당 아이템의
visibility
를hidden
으로 설정합니다. - 드래그가 끝나면
visibility
를 다시visible
로 설정합니다.
- 실시간 정렬 미리보기 구현:
useEffect
를 사용하여 마우스 이동을 추적합니다.- 마우스 위치에 따라
dragOverIndex
를 실시간으로 업데이트합니다. getOrderedTodos
함수를 사용하여 현재 드래그 상태에 따른 정렬된 목록을 생성합니다.
- 성능 최적화:
- 불필요한 리렌더링을 방지하기 위해
draggingIndex
와dragOverIndex
상태를 사용합니다.
- 드래그 상태 관리:
DragState
인터페이스를 정의하여 드래그 관련 상태를 관리합니다.isDragging
,draggedIndex
,draggedOverIndex
,startPosition
을 포함합니다.
- 마우스 이벤트 처리:
handleMouseDown
: 마우스 다운 시 드래그 시작 위치를 기록합니다.handleMouseMove
: 마우스 이동 거리가 15px 이상일 때 드래그를 시작하고, 드래그 중인 아이템의 위치를 업데이트합니다.handleMouseUp
: 드래그가 끝났을 때 아이템 순서를 변경합니다.
- 드래그 헬퍼 구현:
- 드래그 중인 아이템을 표시하는 헬퍼 요소를 추가했습니다.
- 마우스를 따라다니도록
transform
스타일을 사용합니다.
- 시각적 피드백
- 드래그 중인 아이템은
invisible
클래스를 적용하여 원래 위치에서 숨깁니다.
preventDefault()
추가:
handleMouseDown
,handleMouseMove
,handleMouseUp
, 그리고 전역mouseup
이벤트 핸들러에e.preventDefault()
를 추가했습니다. 이는 텍스트 드래그를 방지하고 원치 않는 기본 동작을 막습니다.
- dragHelper 위치 지정 방식 변경:
transform
을 사용하는 대신position: absolute
,top
,left
를 사용하여 dragHelper의 위치를 지정했습니다.z-index
를 1000으로 설정하여 다른 요소들 위에 표시되도록 했습니다.
- DragState 인터페이스 수정:
currentPosition
을 추가하여 드래그 중인 아이템의 현재 위치를 추적합니다.
- 마우스 이벤트 핸들러 수정:
handleMouseMove
에서currentPosition
을 업데이트하도록 변경했습니다.- dragHelper의 위치를
currentPosition
을 기반으로 설정합니다.
- 조건부 렌더링 개선:
- dragHelper 렌더링 조건에
currentPosition
체크를 추가하여 더 안정적으로 표시되도록 했습니다.
- 전역 마우스 이벤트 처리:
mousemove
이벤트를 전역적으로 처리하여 드래그 중 마우스가 컴포넌트를 벗어나도 동작하도록 했습니다.
- 드롭 위치 계산 개선:
getDropIndex
함수를 추가하여 마우스 위치에 따른 드롭 인덱스를 계산합니다.
추가개선점
- 200ms throttle 추가
"use client"
import type React from "react"
import { useState, useRef, useEffect, useCallback } from "react"
import { PlusCircle } from "lucide-react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
interface Todo {
id: number
text: string
}
interface DragState {
isDragging: boolean
draggedIndex: number | null
startPosition: { pageX: number; pageY: number } | null
offset: { left: number; top: number } | null
currentPosition: { x: number; y: number } | null
dropIndex: number | null
}
export default function TodoList() {
const [todos, setTodos] = useState<Todo[]>([])
const [newTodo, setNewTodo] = useState("")
const [dragState, setDragState] = useState<DragState>({
isDragging: false,
draggedIndex: null,
startPosition: null,
offset: null,
currentPosition: null,
dropIndex: null,
})
const [tempTodos, setTempTodos] = useState<Todo[]>([])
const listRef = useRef<HTMLUListElement>(null)
const dragHelperRef = useRef<HTMLDivElement>(null)
const lastDropIndexUpdateTime = useRef<number>(0)
const addTodo = () => {
if (newTodo.trim() !== "") {
setTodos([...todos, { id: Date.now(), text: newTodo }])
setNewTodo("")
}
}
const handleMouseDown = (e: React.MouseEvent<HTMLLIElement>, index: number) => {
e.preventDefault()
const target = e.currentTarget
const rect = target.getBoundingClientRect()
setDragState({
isDragging: false,
draggedIndex: index,
startPosition: { pageX: e.pageX, pageY: e.pageY },
offset: { left: rect.left, top: rect.top },
currentPosition: null,
dropIndex: null,
})
setTempTodos([...todos])
lastDropIndexUpdateTime.current = Date.now()
}
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLLIElement> | MouseEvent) => {
e.preventDefault()
if (dragState.startPosition && dragState.offset && dragState.draggedIndex !== null) {
const currentPosition = {
x: e.clientX - (dragState.startPosition.pageX - dragState.offset.left),
y: e.clientY - (dragState.startPosition.pageY - dragState.offset.top),
}
setDragState((prev) => ({ ...prev, isDragging: true, currentPosition }))
const currentTime = Date.now()
if (currentTime - lastDropIndexUpdateTime.current >= 200) {
// 0.2 seconds throttle
const dropIndex = getDropIndex(e.clientX, e.clientY)
if (dropIndex !== dragState.dropIndex) {
setDragState((prev) => ({ ...prev, dropIndex }))
const newTempTodos = [...todos]
const [draggedItem] = newTempTodos.splice(dragState.draggedIndex, 1)
newTempTodos.splice(dropIndex, 0, draggedItem)
setTempTodos(newTempTodos)
lastDropIndexUpdateTime.current = currentTime
}
}
}
},
[dragState, todos],
)
const handleMouseUp = useCallback(
(e: React.MouseEvent<HTMLLIElement> | MouseEvent) => {
e.preventDefault()
if (dragState.isDragging) {
setTodos(tempTodos)
}
setDragState({
isDragging: false,
draggedIndex: null,
startPosition: null,
offset: null,
currentPosition: null,
dropIndex: null,
})
},
[dragState.isDragging, tempTodos],
)
const getDropIndex = (x: number, y: number): number => {
if (!listRef.current) return -1
const items = Array.from(listRef.current.children) as HTMLLIElement[]
return items.findIndex((item) => {
const rect = item.getBoundingClientRect()
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
})
}
useEffect(() => {
document.body.style.cursor = dragState.isDragging ? "ew-resize" : "default"
}, [dragState.isDragging])
useEffect(() => {
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
}, [handleMouseMove, handleMouseUp])
return (
<div className="max-w-4xl mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-4">Todo List</h1>
<div className="flex mb-4">
<Input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new todo"
className="flex-grow mr-2"
/>
<Button onClick={addTodo}>
<PlusCircle className="w-4 h-4 mr-2" />
Add
</Button>
</div>
<ul ref={listRef} className="flex flex-wrap gap-2 items-start">
{(dragState.isDragging ? tempTodos : todos).map((todo, index) => (
<li
key={todo.id}
onMouseDown={(e) => handleMouseDown(e, index)}
className={`p-2 bg-gray-100 rounded transition-colors inline-block ${
dragState.isDragging && index === dragState.dropIndex ? "invisible" : ""
}`}
>
{todo.text}
</li>
))}
</ul>
{dragState.isDragging && dragState.draggedIndex !== null && dragState.currentPosition && (
<div
ref={dragHelperRef}
className="fixed pointer-events-none bg-gray-200 p-2 rounded opacity-80"
style={{
position: "absolute",
top: `${dragState.currentPosition.y}px`,
left: `${dragState.currentPosition.x}px`,
zIndex: 1000,
}}
>
{todos[dragState.draggedIndex].text}
</div>
)}
</div>
)
}
참고
drag and drop 기능 구현과 최적화까지
drag and drop 직접 구현해본 적이 있나요?
velog.io
반응형
'React.js' 카테고리의 다른 글
[React] state prev 는 무엇인가? (0) | 2025.02.25 |
---|---|
[React] MutableRefObject와 LegacyRef (1) | 2025.01.21 |
[React.19] 메모이제이션이 필요 없는 리액트 컴파일러 (0) | 2024.10.18 |
React.memo, useMemo, useCallback 역할 및 차이점 (0) | 2024.10.02 |
[React] forwardRef란? (1) | 2024.09.02 |