본문 바로가기
카테고리 없음

React로 구현하는 드래그 가능한 그리드 대시보드

by 주갬 2025. 1. 2.
반응형

 

소개

 

최근 대시보드나 캔버스 기반 애플리케이션에서 자주 볼 수 있는 그리드 기반 드래그 앤 드롭 시스템을 React로 구현하는 방법을 알아보겠습니다. 이 글에서는 dnd-kit과 react-resizable을 사용하여 다음 기능들을 구현할 것입니다:

  • 그리드에 맞춘 드래그 앤 드롭
  • 아이템 크기 조절
  • 충돌 감지
  • 반응형 그리드 시스템

 

기술 스택

  • React
  • @dnd-kit/core: 드래그 앤 드롭 기능 구현
  • react-resizable: 크기 조절 기능 구현
  • Material-UI Icons: UI 아이콘

 

핵심 컴포넌트 구조

 

프로젝트는 크게 두 개의 주요 컴포넌트로 구성됩니다:

  1. Widget.jsx: 전체 그리드 시스템을 관리하는 컴포넌트입니다. 이 컴포넌트는 그리드의 상태를 관리하고, 드래그 앤 드롭 로직을 처리합니다.
  2. 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
        ));
    }
}
  1. 드래그 된 거리(delta)를 받아옵니다.
  2. 현재 픽셀 위치를 계산합니다.
  3. 픽셀 위치를 그리드 위치로 변환합니다.
  4. 유효한 위치인지 확인 후 상태를 업데이트합니다.

 

2. 충돌 감지

 

isValidPosition 함수는 새로운 위치가 유효한지 검사합니다. 이 함수는 두 가지를 체크합니다:

  1. 아이템이 그리드 경계를 벗어나는지 확인
  2. 다른 아이템과 겹치는지 확인
  3. 충돌이 발생하면 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 함수는 아이템의 크기 조절을 담당합니다. 이 함수는:

  1. 새로운 크기를 그리드 단위로 변환합니다.
  2. 최소 크기(1x1)를 보장합니다.
  3. 충돌 검사를 수행합니다.
  4. 유효한 경우에만 크기를 업데이트합니다.
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를 사용합니다. 이 훅은:

  1. 컨테이너의 너비를 측정합니다.
  2. 그리드 열 너비를 계산합니다.
  3. 리사이즈 이벤트를 감지하여 그리드를 업데이트합니다.
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);
}, []);

 

 


 

활용 방안

 

이렇게 구현된 그리드 시스템은 다양한 용도로 활용될 수 있습니다:

  1. 대시보드 구현
  2. 드래그 가능한 위젯 시스템
  3. 칸반 보드
  4. 커스텀 레이아웃 에디터

추가 가능한 기능들:

  • 여러 아이템 동시 선택 및 이동
  • 저장 및 불러오기 기능
  • 실시간 협업 기능
  • 아이템 타입별 다른 크기 제한

이렇게 구현된 그리드 시스템은 사용자가 직관적으로 콘텐츠를 구성하고 관리할 수 있게 해주며, 다양한 프로젝트에 활용될 수 있습니다.

 

 

이 글은 클로드AI의 도움을 받아 작성하였습니다

반응형