소개
최근 대시보드나 캔버스 기반 애플리케이션에서 자주 볼 수 있는 그리드 기반 드래그 앤 드롭 시스템을 React로 구현하는 방법을 알아보겠습니다. 이 글에서는 dnd-kit과 react-resizable을 사용하여 다음 기능들을 구현할 것입니다:
- 그리드에 맞춘 드래그 앤 드롭
- 아이템 크기 조절
- 충돌 감지
- 반응형 그리드 시스템
기술 스택
- React
- @dnd-kit/core: 드래그 앤 드롭 기능 구현
- react-resizable: 크기 조절 기능 구현
- Material-UI Icons: UI 아이콘
핵심 컴포넌트 구조
프로젝트는 크게 두 개의 주요 컴포넌트로 구성됩니다:
- Widget.jsx: 전체 그리드 시스템을 관리하는 컴포넌트입니다. 이 컴포넌트는 그리드의 상태를 관리하고, 드래그 앤 드롭 로직을 처리합니다.
- ResizableDraggable.jsx: 개별 드래그 가능한 아이템을 구현한 컴포넌트입니다. 각 아이템의 드래그와 리사이즈 기능을 담당합니다.
그리드 시스템 설계
1. 기본 설정
const gridCol = 10; // 그리드 열 수
const gridHeight = 80; // 각 그리드 셀의 높이
const gap = 10; // 그리드 간격
이러한 상수들은 그리드의 기본적인 레이아웃을 결정합니다. gridCol은 전체 그리드의 열 수를, gridHeight는 각 셀의 높이를, gap은 셀 간의 간격을 정의합니다.
2. 좌표 시스템
우리의 그리드 시스템은 두 가지 좌표계를 사용합니다:
- 픽셀 좌표: 실제 화면상의 위치를 나타내는 좌표입니다.
- 그리드 좌표: 논리적인 그리드 위치를 나타내는 좌표입니다. (0,0), (1,0) 등의 형태로 표현됩니다.
이 두 좌표계 사이의 변환은 calculateGridPosition 함수가 담당합니다:
const calculateGridPosition = (pixelX, pixelY) => {
const gridX = Math.round(pixelX / gridWidth);
const gridY = Math.round(pixelY / (gridHeight + gap));
return { x: gridX, y: gridY };
};
이 함수는 픽셀 단위의 위치를 받아서 해당하는 그리드 좌표로 변환해줍니다.
핵심 기능 구현
1. 드래그 앤 드롭
드래그 앤 드롭의 핵심은 handleDragEnd 함수입니다. 이 함수는 사용자가 드래그를 끝냈을 때 호출되며, 다음과 같은 과정을 거칩니다.
function handleDragEnd(event) {
const { delta, active } = event;
const currentItem = data.find(item => item.id === active.id);
if (!currentItem) return;
const currentPixelX = currentItem.position.x * gridWidth + delta.x;
const currentPixelY = currentItem.position.y * (gridHeight + gap) + delta.y;
const newGridPosition = calculateGridPosition(currentPixelX, currentPixelY);
if (isValidPosition(newGridPosition, currentItem.id, currentItem.scale)) {
setData(prevData => prevData.map(item =>
item.id === active.id
? { ...item, position: newGridPosition }
: item
));
}
}
- 드래그 된 거리(delta)를 받아옵니다.
- 현재 픽셀 위치를 계산합니다.
- 픽셀 위치를 그리드 위치로 변환합니다.
- 유효한 위치인지 확인 후 상태를 업데이트합니다.
2. 충돌 감지
isValidPosition 함수는 새로운 위치가 유효한지 검사합니다. 이 함수는 두 가지를 체크합니다:
- 아이템이 그리드 경계를 벗어나는지 확인
- 다른 아이템과 겹치는지 확인
- 충돌이 발생하면 false를 반환하여 해당 위치로의 이동을 막습니다.
const isValidPosition = (newPosition, itemId, itemScale) => {
// 그리드 경계 체크
if (newPosition.x < 0 || newPosition.y < 0 ||
newPosition.x + itemScale.width > gridCol) {
return false;
}
// 다른 아이템과의 충돌 체크
return !data.some(item => {
if (item.id === itemId) return false;
const itemRight = item.position.x + item.scale.width;
const itemBottom = item.position.y + item.scale.height;
const newRight = newPosition.x + itemScale.width;
const newBottom = newPosition.y + itemScale.height;
return !(newPosition.x >= itemRight ||
newPosition.y >= itemBottom ||
item.position.x >= newRight ||
item.position.y >= newBottom);
});
};
3. 크기 조절
ResizableDraggable 컴포넌트의 handleResize 함수는 아이템의 크기 조절을 담당합니다. 이 함수는:
- 새로운 크기를 그리드 단위로 변환합니다.
- 최소 크기(1x1)를 보장합니다.
- 충돌 검사를 수행합니다.
- 유효한 경우에만 크기를 업데이트합니다.
const handleResize = (e, { size: newSize }) => {
const newScaleWidth = Math.round(newSize.width / (gridWidth - gap));
const newScaleHeight = Math.round(newSize.height / gridHeight);
const newScale = {
width: Math.max(1, newScaleWidth),
height: Math.max(1, newScaleHeight)
};
if (isValidPosition(item.position, item.id, newScale)) {
setData(prevData => prevData.map(dataItem =>
dataItem.id === item.id
? { ...dataItem, scale: newScale }
: dataItem
));
}
};
데이터 구조
각 아이템은 다음과 같은 구조를 가집니다.
{
id: number,
position: { x: number, y: number }, // 그리드 좌표
text: string, // 아이템 내용
scale: { width: number, height: number } // 그리드 단위의 크기
}
반응형 처리
윈도우 크기가 변경될 때 그리드를 적절히 조정하기 위해 useEffect를 사용합니다. 이 훅은:
- 컨테이너의 너비를 측정합니다.
- 그리드 열 너비를 계산합니다.
- 리사이즈 이벤트를 감지하여 그리드를 업데이트합니다.
useEffect(() => {
const updateContainerWidth = () => {
const container = document.querySelector('.dashboard-container');
const newContainerWidth = container ? container.offsetWidth - 59 : 0;
setGridWidth(newContainerWidth / gridCol);
};
window.addEventListener('resize', updateContainerWidth);
updateContainerWidth();
return () => window.removeEventListener('resize', updateContainerWidth);
}, []);
활용 방안
이렇게 구현된 그리드 시스템은 다양한 용도로 활용될 수 있습니다:
- 대시보드 구현
- 드래그 가능한 위젯 시스템
- 칸반 보드
- 커스텀 레이아웃 에디터
추가 가능한 기능들:
- 여러 아이템 동시 선택 및 이동
- 저장 및 불러오기 기능
- 실시간 협업 기능
- 아이템 타입별 다른 크기 제한
이렇게 구현된 그리드 시스템은 사용자가 직관적으로 콘텐츠를 구성하고 관리할 수 있게 해주며, 다양한 프로젝트에 활용될 수 있습니다.
이 글은 클로드AI의 도움을 받아 작성하였습니다