개발자
류준열
바탕화면 구현을 위해 Draggable 컴포넌트 만들기
바탕화면에서 아이콘을 드래그 할 수 있도록 Draggable 컴포넌트를 만들었다.

그리고 이 Draggable 컴포넌트를 재사용하여 폴더내에서도 똑같은 기능을 구현하였다.

코드는 다음과 같다.
// 바탕화면
export const Home = () => {
...
return (
<div className={styles.home}>
<Draggable icons={ICONS} />
...
</div>
);
};
// window 창 : 폴더이면 Draggable, 파일이면 markdown
export const WindowBox = ({ icon, index }: WindowBoxProps) => {
...
return (
<section>
{icon.type===IconType.FOLDER ? <Draggable icons={icon.children || []} /> : <Markdown markdown={icon.markdown} />
</section>
}
Draggable 컴포넌트 내부
Draggable 컴포넌트 내부는 2개의 로컬state와 1개의 전역 state가 있다.
export const Draggable = ({ icons: _icons }: DraggableProps) => {
const [draggingIcon, setDraggingIcon] = useState<{ id: number } | null>(null);
const isDraggable = useDraggableStore(state => state.isDraggable);
const [icons, setIcons] = useState(_icons);
...
- draggingIcon은 지금 끌고 있는 icon
- isDraggable은 드래그 가능한 상태인지를 나타내는 boolean값이다. 이름변경, 우클릭 등이 발생했을때는 false이다.
- icons는 현재 Draggable내에 있는 각 icon들의 이름,위치,zIndex등을 갖고 있는 state이다. 아래는 예시코드
const ICONS = [ {
type: 'file',
windowState: 'closed',
id: 5,
src: CodestatesLogo,
alt: '지원선발 시스템',
left: 50,
top: 50,
zIndex: 100,
markdown: '/markdown/admission-admin.md',
},{
type: 'folder',
windowState: 'closed',
id: 3,
src: FolderSVG,
alt: '프로젝트',
left: 50,
top: 350,
zIndex: 100,
children: [
{
type: 'file',
windowState: 'closed',
id: 4,
src: CodestatesLogo,
alt: '코드스테이츠 랜딩페이지',
left: 50,
top: 150,
zIndex: 100,
markdown: '/markdown/landing-page.md',
}]}],
Icon을 드래그 하기 시작 할 때 : onDragStart
드래그 할 Icon.tsx는 다음과 같다.
export const Icon = () => {
return (
<div draggable={isDraggable} onDragStart={e=>handleDragStart(e,icon.id}>
<img className = 'icon_img' .. />
<input className = 'icon_name' .. />
</div>
)
}
Icon을 드래그 할 때 실행되는 handleDragStart는 dragEvent와 icon.id를 매개변수로 받고 다음과 같이 작동한다.
export const handleDragStart = (
e: DragEvent,
id: number
) => {
if (isDraggable) {
// 드래그 중인 아이콘을 확인하고 이동한 위치에 해당 아이콘을 배치하기 위해 id를 추적한다.
e.dataTransfer.setData('id', id.toString());
setDraggingIcon({ id });
}
};
Icon을 Draggable 컴포넌트 상에서 드래그 할 때 : onDragover
일단 Dragglable.tsx 컴포넌트 리턴문은 다음과 같다.
export const Draggable = () => {
...
return (
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<div>
{icons.map(icon=>(<Icon key = {icon.id} .... />))}
</div>
</div>)
}
onDragOver는 MDN에서 보면 엘리먼트가 유효한 드래그 영역 내에서 드래그 될 때 every few hundreds 타임마다 발생하는 이벤트이다.
브라우저 고유의 이벤트가 발생하는 것을 방지하기 위해 e.prevetDefault()만 깔아주었다.
export const Draggable = () => {
...
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
};
...
return (
<div>
onDragOver={handleDragOver}
onDrop={handleDrop}
</div>)
}
Icon을 Drop 할 때 : onDrop
그리고 drop할 때 발생하는 handleDrop까지 추가해주었다.
역시 브라우저 고유의 이벤트때문에 방해받지 않도록 e.preventDefault()를 사용했다.
그리고 Icon.tsx 의 onDragStart에서 추적하던 icon.id를 받아서 drop할 icon를 결정하고
drag event에 내장된 마우스 위치를 icon.left, icon.top에 할당한다.
export const Draggable = () => {
...
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
};
const handleDrop = (
e: DragEvent
) => {
if (isDraggable) {
e.preventDefault();
if (draggingIcon) {
const id = Number(e.dataTransfer.getData('id'));
const icon = icons.find(i => i.id === id);
if (icon) {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const updatedIcons = icons.map(i => {
if (i.id === id) {
return { ...i, left: x - 30, top: y - 25 };
}
return i;
});
setIcons(updatedIcons);
}
setDraggingIcon(null);
}
}
};
...
return (
<div>
onDragOver={handleDragOver}
onDrop={handleDrop}
</div>)
}
그리고 icon.left와 icon.top을 Icon.tsx에서 Icon의 left,top값으로 넣어준다.
export const Icon = (...) => {
return (
<>
<div
draggable={isDraggable}
onDragStart={e => handleDragStart(e, icon.id)}
style={{
left: icon.left,
top: icon.top,
}}
>
...
</div>
</>
이렇게 하여 Draggable.tsx를 만들었다.
Draggable 컴포넌트 예시
실제 구현체는 더 많은 코드가 있는데, Icon을 Drag하는 기능만 들어간 Draggable 컴포넌트는 다음과 같다.
import React, { useState } from 'react';
const Desktop: React.FC = () => {
const [icons, setIcons] = useState([
{ id: 1, icon: '🌟', left: 50, top: 50 },
{ id: 2, icon: '📁', left: 150, top: 50 },
{ id: 3, icon: '📎', left: 250, top: 50 },
]);
const [draggingIcon, setDraggingIcon] = useState<{ id: number } | null>(null);
const handleDragStart = (e: React.DragEvent, id: number) => {
e.dataTransfer.setData('id', id.toString());
setDraggingIcon({ id });
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
if (draggingIcon) {
const id = Number(e.dataTransfer.getData('id'));
const icon = icons.find((i) => i.id === id);
if (icon) {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const updatedIcons = icons.map((i) => {
if (i.id === id) {
return { ...i, left: x, top: y };
}
return i;
});
setIcons(updatedIcons);
}
setDraggingIcon(null);
}
};
const iconElements = icons.map((icon) => (
<div
key={icon.id}
style={{
position: 'absolute',
left: icon.left,
top: icon.top,
cursor: 'pointer',
}}
draggable
onDragStart={(e) => handleDragStart(e, icon.id)}
>
{icon.icon}
</div>
));
return (
<div
style={{
width: '100%',
height: '100%',
position: 'relative',
backgroundColor: 'lightgray',
}}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{iconElements}
</div>
);
};
export default Desktop;