개발자
류준열
바탕화면 구현을 위해 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;