motion 프로젝트를 진행 하면서, 드래그앤 드롭시 아래에 있는 컴포넌트 색상을 바꾸고 싶어서 찾아본 것에 대해 기록하고자 한다.
오늘 하루를 꼬박 다 써서 찾았는데 의도한 대로 작동하지 않아 애를 먹었다. 처음에 했던 방식으로는, 드래그를 시작한 컴포넌트가 계속 색상이 바뀌어 있거나, 아예 바뀌지 않는 등 여러 방법으로 시행 착오를 꽤 겪었었다.
결론은 mdn 문서에 정답이 있었고, 앞으로 잘 모르는 기능에 대해선 무조건 공식 문서 및 mdn 문서를 찾아본 뒤 해결하는 습관을 필수로 길러야겠다.
처음에는 onDragEnter의 존재를 모르고 getDragId 같은 함수를 만든 뒤, getData로 아이디를 알아내고, 현재 아이디와 getDragId 함수의 리턴 값의 아이디가 같다면 색상이 변경되게 만들어야 하는 줄 알고 잘못된 코드를 썼었다.
// 잘못된 코드
const getDragId = (event: React.DragEvent<HTMLDivElement>) => {
const dropId = Number(event.dataTransfer.getData("text/plain"));
return dropId;
};
<div
draggable
onDragStart={(event) => {
onDragStart(event, id.toString());
const newDragId = getDragId(event);
setDragId(newDragId); // useState로 dragId 값을 업데이트
console.log("id:", id);
console.log("dragId:", dragId);
}}
onDragOver={(event) => onDragOver(event)}
onDrop={(event) => {
onDrop(event, index);
}}
className={`flex post p-5 m-3 border hover:border-red-200 rounded-lg relative ${
dragId === id ? "bg-red-200" : "" // dragId와 id 값이 같으면 배경 색상을 변경하게 만들었다
}`}
>
이렇게 썼더니 드래그를 시도한 모든 게시글의 배경이 bg-red-200 색상으로 바뀌었고, 드래그를 놓아도 bg-red-200 색상에서 변경되지 않았다.
내가 의도했던 동작과 다르게 되어 더 찾아본 결과 onDragEnter라는 기능이 있음을 알게 되었다.
onDragEnter
는 JavaScript에서 제공하는 이벤트 핸들러 중 하나입니다. 이 이벤트 핸들러는 드래그된 요소가 다른 요소 위로 들어갈 때 발생합니다. 일반적으로 드래그 앤 드롭(Drag and Drop) 기능을 구현할 때 사용되며, 드래그된 요소가 드롭 대상 요소 위로 진입할 때 원하는 동작을 수행할 수 있습니다.
예를 들어, 사용자가 마우스로 요소를 드래그하면 해당 요소에
onDragEnter
이벤트가 발생합니다. 이 때, 드래그되는 요소가 다른 요소 위로 진입할 때마다onDragEnter
이벤트 핸들러가 호출됩니다. 이를 통해 드롭 대상 요소에 특정 스타일을 적용하거나 상태를 변경하는 등의 작업을 수행할 수 있습니다.
onDragEnter
이벤트는 HTML5의 드래그 앤 드롭 API와 관련이 있으며, 일반적으로<div>
,<span>
,<li>
와 같은 요소에서 사용됩니다. 이벤트 객체를 통해 드래그 중인 요소와 진입한 요소의 정보를 확인하고, 원하는 작업을 수행할 수 있습니다.
mdn에서도 드래그 앤 드롭 관련 참고 자료를 찾을 수 있었다.
https://developer.mozilla.org/ko/docs/Web/API/HTML_Drag_and_Drop_API
그래서 useDrag훅에setDragEnteredId를 사용할 수 있도록 인터페이스를 만들고,
useDrag에 onDragEnter를 다음과 같이 작성했다.
import { postList } from "../store/content";
import { DragPropsType } from "../types/contentsType";
interface Props extends DragPropsType {
setDragEnteredId: React.Dispatch<React.SetStateAction<number | null>>;
}
export const useDrag = ({ posts, dispatch, setDragEnteredId }: Props) => {
// onDragEnter : 마우스가 컨텐츠 위로 들어갈 때 dragId 값을 업데이트
const onDragEnter = (
event: React.DragEvent<HTMLDivElement>,
enteredId: number
) => {
event.preventDefault();
setDragEnteredId(enteredId);
};
// onDragStart : 드래그 이벤트를 처리하는 함수
// 이벤트 객체의 dataTransfer 프로퍼티에 id를 text/plain 형태로 저장
// 이렇게 하면 드래그된 아이템을 드랍할 때 id를 전달할 수 있다
const onDragStart = (
event: React.DragEvent<HTMLDivElement>,
dragId: string
) => {
event.dataTransfer.setData("text/plain", dragId);
};
const onDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
};
// onDrop : 드롭 이벤트를 처리하는 함수
// 드롭 대상 요소에서 호출됨. onDrop 이벤트 핸들러를 등록하면 해당 요소 위에 드래그된 아이템이 등록될 때 이 함수가 호출된다
// 매개변수로 이벤트 객체와 index를 받는다
// index : 드롭 대상 요소에서 해당 아이템이 드롭된 위치의 인덱스를 나타냄
// index를 이용해 드롭된 아이템을 원하는 위치에 삽입 가능
// event.dataTransfer.getData("text/plain"): 드래그 이벤트가 발생했을 때 설정한 데이터를 가져옴
// state의 items에서 id와, 드래그 이벤트에서 설정한 데이터의 id가 같은 것을 찾아서
// item배열에서 item의 id와 일치하지 않는 것을 찾아 newItems를 만든 뒤
// index 위치에 0개 요소를 삭제하고 item 요소를 추가
// setItems 함수를 호출, items 배열을 newItems로 업데이트
const onDrop = (
event: React.DragEvent<HTMLDivElement>,
dropIndex?: number
) => {
event.preventDefault();
if (dropIndex === undefined) {
return;
}
const dropId = Number(event.dataTransfer.getData("text/plain"));
const item = posts.find((it) => it.id === dropId);
if (item) {
const newItems = posts.filter((it) => it.id !== dropId);
newItems.splice(dropIndex, 0, item);
dispatch(postList(newItems));
}
setDragEnteredId(null);
};
return {
onDragStart,
onDragOver,
onDrop,
onDragEnter,
};
};
이제 사용할 컴포넌트로 넘어가서, 상위 컴포넌트인 ContentList에 state를 지정하고 넘겨줬다.
const [dragEnteredId, setDragEnteredId] = useState<number | null>(null);
//...
return (
<>
<div className=" flex flex-col items-center justify-center">
{posts.length === 0 && (
<div className="items-center justify-center text-gray-500">
등록된 포스트가 없습니다.
</div>
)}
<ul className="w-full">
{posts.map((post: Post, index: number) => (
<ContentListItem
key={post.id}
category={post.category}
id={post.id}
imageUrl={post.imageUrl}
videoUrl={post.videoUrl}
task={post.task}
title={post.title}
content={post.content}
index={index}
dragEnteredId={dragEnteredId}
setDragEnteredId={setDragEnteredId}
/>
))}
</ul>
</div>
</>
);
그 다음 하위 컴포넌트인 ContentListItem.tsx에서 Props 인터페이스에 dragEnteredId와 setDragEnteredId를 추가했다.
interface Props {
id: number;
imageUrl?: string | null | undefined;
videoUrl?: string | null | undefined;
task?: boolean | undefined;
title: string;
content: string;
index?: number | undefined;
category?: string;
dragEnteredId: number | null;
setDragEnteredId: React.Dispatch<React.SetStateAction<number | null>>;
}
ContentList에서 받아온 dragEnteredId와 setDragEnteredId를 가져오고,
useDrag의 onDragEnter 함수도 가져온다.
const ContentListItem = ({
id,
imageUrl,
videoUrl,
task,
title,
content,
index,
category,
dragEnteredId,
setDragEnteredId,
}: Props) => {
//...
const { onDragStart, onDragOver, onDrop, onDragEnter } = useDrag({
posts,
dispatch,
setDragEnteredId,
});
마지막으로 리턴문의 onDragEnter에서 onDragEnter에 필요한 값을 전달해주고, className의 속성을 변경했다.
return (
<>
<li className="flex-grow relative">
<div
draggable
onDragStart={(event) => {
onDragStart(event, id.toString());
}}
onDragEnter={(event) => onDragEnter(event, id)}
onDragOver={(event) => onDragOver(event)}
onDrop={(event) => {
onDrop(event, index);
}}
className={`flex post p-5 m-3 border hover:border-red-200 rounded-lg relative ${
dragEnteredId === id ? "bg-red-200" : ""
}`}
>
dragEnteredId와 id가 동일하면 배경 색상이 바뀌고, 그렇지 않으면 배경 색상의 변화가 없게 만들어줬다.
이제 의도한 대로 정상적으로 작동하는 것을 확인할 수 있었다 :)