드래그앤드롭(큐를 이용한 쓰로틀링)

최훈오·2023년 11월 4일
0

데브코스

목록 보기
7/29
post-thumbnail

드래그앤드롭

TodoList.js 리스트

$todoList.setAttribute("droppable", "true");

<li data-id="${todo._id}"draggable="true">${todo.content} <button>x</button></li>

li태그를 담는 상위 태그에 요소를 끌어당길 수 있도록 droppable속성을 추가하고,
draggable 속성을 추가해 li 태그가 드래그 가능한 요소로 바꾼다.

TodoList.js 이벤트

	$todoList.addEventListener("dragstart", (e) => {
		const $li = e.target.closest("li");
		e.dataTransfer.setData("todoId", $li.dataset.id);
	});

	$todoList.addEventListener("dragover", (e) => {
		e.preventDefault();

		e.dataTransfer.dropEffect = "move";
	});

	$todoList.addEventListener("drop", (e) => {
		e.preventDefault();
		const droppedTodoId = e.dataTransfer.getData("todoId");

		// 현재 TodoList의 Todo가 아닌 경우 상위 컴포넌트 알림
		const { todos } = this.state;
		if (!todos.find((todo) => todo._id === droppedTodoId)) {
			onDrop(droppedTodoId);
		}
	});

드래그앤드롭 이벤트 기능은 dragstart, dragover, drop세가지 요소로 구현할 수 있다.

  • dragstart : 드래그가 시작됐을때 발생, 로컬스토리지나 객체에 값을 집어넣듯이 setData를 이용해 드래그가 시작된 곳의 id를 저장한다.

  • dragover : 드래그하면서 마우스가 대상 객체의 위에 자리 잡고 있을 때 발생,기본적으로 html 요소는 다른 요소의 위에 위치할 수 없어서 event.preventdefault()를 통해 다른 요 위에 위치할 수 있도록 만들어야한다.
    시각적인 힌트를 주는 effect 속성에는 세가지가 있다.

    • copy : 복사를 나타냄
    • link : 링크가 생성됨
    • move : 이동을 나타냄
  • drop : 드래그하던 객체를 놓으면 발생, 이벤트가 발생한 경우 어디서 드래그를 했는지 getData를 통해 id를 알아내고, 상위컴포넌트에 id전달

App.js

onDrop: async (todoId) => {
	await request(`/${todoId}/toggle`, {
					method: "PUT",
    });
	await fetchTodos();
},

드래그가 발생한 곳의 id를 인자로 전달받아 서버에 요청하여 데이터를 받아오고 리스트를 업데이트 시킨다.

여기까지가 기본 기능이다. 하지만, 매번 드래그앤드롭을 할때마다 서버에 요청을 보내고 다시 렌더링 하는 방식이라 서버와의 통신이 느릴 경우 UI 업데이트가 느리다는 문제점이 있다.
낙관적 업데이트를 도입해보자.

낙관적 업데이트

onDrop: async (todoId) => {
// 낙관적 업데이트
	const nextTodos = [...this.state.todos];
	const todoIndex = nextTodos.findIndex((todo) => todo._id === todoId);
	nextTodos[todoIndex].isCompleted = false; // true
	this.setState({
				...this.state,
				todos: nextTodos,
				});

	await request(`/${todoId}/toggle`, {
				method: "PUT",
				});

	await fetchTodos();
},

서버에 요청을 보내기 전에 받은 id를 통해 먼저 UI에 그리는 식으로 낙관적 업데이트를 구현하였다.

하지만, 이런 요청들을 빠르게 보내다보면 UI가 꼬이는 현상이 발생하는 문제가 생긴다.

이 문제를 태스크 큐를 이용해 해결해보자.

큐 이용

onDrop: async (todoId) => {
  // 낙관적 업데이트
		const nextTodos = [...this.state.todos];
		const todoIndex = nextTodos.findIndex((todo) => todo._id === todoId);
		nextTodos[todoIndex].isCompleted = true; // false
		this.setState({
			...this.state,
			todos: nextTodos,
			});

		tasks.addTask(async () => {
			await request(`/${todoId}/toggle`, {
					method: "PUT",
					});
				});
			},
export default function TaskManagaer() {
	const tasks = [];

	this.addTask = (task) => {
		tasks.push(task);
		console.log(tasks);
	};

	this.run = async () => {
		if (tasks.length > 0) {
			// 가장 나중에 들어온 것부터 먼저 실행
			const task = tasks.shift();
			await task();
			this.run();
		}
	};
}
const $button = document.createElement("button");
$button.textContent = "변경내용 동기화";
$target.appendChild($button);

$button.addEventListener("click", () => tasks.run());

현재 백그라운드에서 너무 많은 작업들이 일어나므로 작업을 모아놨다가 한번에 실행하는 방법을 큐로 구현하였다. TaskManager에서 큐를 관리한다.
순서는 다음과 같다.

  1. 이벤트가 발생할때마다 addTask를 통해 큐에 서버요청을 저장한다.
  2. 큐에는 순서대로 들어온 순서부터 서버요청을 차곡차곡 저장한다.
  3. 이후에 버튼 클릭을 통해 run을 실행하면 큐가 모두 비워질때까지 저장해놨던 서버 요청을 수행한다.

하지만 이 방식도 성능에 문제가 있다.
한 요소를 가지고 여러번 옮긴 다음에 그 요소를 삭제하는 경우 삭제 이전의 서버 요청은 쓸모가 없는 요청이 된다. 따라서 삭제 이전의 요청을 모두 삭제하는 기능을 구현해보자.

큐 이용2

export default function SyncTaskManager() {
	let tasks = [];

	this.addTask = (task) => {
		tasks.push(task);
	};

	// url이 포함되는 경우(하나에 대해서 여러번 옮기고 삭제하면 삭제이전 요청 모두 삭제)
	this.removeTasks = (urlPattern) => {
		tasks = tasks.filter((task) => !task.url.includes(urlPattern));
	};

	this.run = async () => {
		if (tasks.length > 0) {
			const task = tasks.shift();
			await request(task.url, {
				method: task.method || "GET",
			});
			this.run();
		}
	};
}
const handleTodoRemove = (todoId) => {
  const nextTodos = [...this.state.todos];
  const todoIndex = nextTodos.findIndex((todo) => todo._id === todoId);
  nextTodos.splice(todoIndex, 1);

  this.setState({
    ...this.state,
    todos: nextTodos,
  });

  tasks.removeTasks(`/${todoId}`);
  tasks.addTask({
    url: `/${todoId}`,
    method: "DELETE",
  });
};

삭제 버튼을 누를 경우 큐 안의 tasks를 순회하면서 방금 들어온 taskurl이 같은 task를 모두 없애며 문제를 해결하였다. 추가적으로 addTask을 호출할때, url,method를 인자로 넣어 태스크큐 컴포넌트 내부에서 동적으로 관리하도록 로직을 변경하였다.

그외

requestIdleCallback, web worker(메인 스레드 바깥에서 돌아가므로 성능이 더 좋음)같은 기능을 통해 이런 문제들을 해결할 수 있다고 한다.

마치며

이로써 바닐라 JS를 통해 여러가지 기능을 만들어 봤다. 처음에 바닐라 JS라길래 학교 수업에서 배웠던 방식처럼 DOM요소에 복잡하게 접근하여 하드코딩 하는 방식을 떠올렸는데 리액트와 같은 라이브러리처럼 컴포넌트 단위로 구현하고, 형제 컴포넌트 끼리는 의존성을 배제하면서 전체적으로 렌더링을 하는 방식이 신기했다. 리액트 이전에도 이렇게 구현할 수 있었고, 이런 흐름이 있었는지 느낄 수 있었다.

초반에 노션 클로닝 까지가 이해하기가 어렵다고 느꼈는데 그 다음 강의를 들으며 차근차근 코드를 여러번 보다보니 익숙해져서 다행이다. 이제 CSS를 배우게 되는데 빨리 지금까지 배운 이해를 토대로 덜 몰입했다고 느껴지는 노션클로닝 프로젝트를 보완해야 겠다.

0개의 댓글