Skip to content

DndGrid Usage Guide

DndGrid는 선언적인 JSX 문법을 통해 복잡한 분할 레이아웃(Split Layout)을 구성하고, 드래그 앤 드롭(Drag & Drop)을 통해 레이아웃을 실시간으로 변경할 수 있는 React 컴포넌트 라이브러리입니다.


1. 개요

DndGrid는 다음과 같은 사용자 경험을 제공합니다:

  • 직관적인 레이아웃 정의: HTML/JSX와 흡사한 구조로 복잡한 그리드를 설명할 수 있습니다.
  • 인터랙티브한 재구조화: 사용자가 직접 영역을 드래그하여 레이아웃을 바꿀 수 있습니다.
  • 컴포넌트 상태 보존: 레이아웃이 변경되어도 내부 컴포넌트의 입력값이나 스크롤 위치 등 React 상태가 유지됩니다.

2. 시작하기

설치

bash
npm install zerojin

AI 도구 활용 (MCP)

설치 방법

해당 프로젝트의 Claude Desktop, Gemini CLI 등 MCP 클라이언트 설정 파일에 다음을 추가하세요:

json
{
    "mcpServers": {
        "dndgrid": {
            "command": "npx",
            "args": ["-y", "mcp-dndgrid"]
        }
    }
}

사용 방법

bash
dndgrid mcp를 사용해서 UserStats, SalesChart,ActivityFeed,RecentOrders 2*2 레이아웃 생성해줘

생성 결과

tsx
import {
    DndGridContainer,
    DndGridSplit,
    DndGridItem,
} from 'zerojin/components';

function MyDashboard() {
    return (
        <DndGridContainer width={600} height={600}>
            <DndGridSplit direction="horizontal" ratio={0.5}>
                <DndGridSplit direction="vertical" ratio={0.5}>
                    <DndGridItem>
                        <ItemDrag className="h-full">
                            <DndGridItemContent className="h-full">
                                <UserStats />
                            </DndGridItemContent>
                        </ItemDrag>
                    </DndGridItem>
                    <DndGridItem>
                        <ItemDrag className="h-full">
                            <DndGridItemContent className="h-full">
                                <SalesChart />
                            </DndGridItemContent>
                        </ItemDrag>
                    </DndGridItem>
                </DndGridSplit>
                <DndGridSplit direction="vertical" ratio={0.5}>
                    <DndGridItem>
                        <ItemDrag className="h-full">
                            <DndGridItemContent className="h-full">
                                <ActivityFeed />
                            </DndGridItemContent>
                        </ItemDrag>
                    </DndGridItem>
                    <DndGridItem>
                        <ItemDrag className="h-full">
                            <DndGridItemContent className="h-full">
                                <RecentOrders />
                            </DndGridItemContent>
                        </ItemDrag>
                    </DndGridItem>
                </DndGridSplit>
            </DndGridSplit>
        </DndGridContainer>
    );
}

실행 결과

아래에서 DndGrid를 직접 사용해보세요. 패널을 드래그하여 레이아웃을 변경할 수 있습니다.

사용 팁

  • 각 패널을 클릭하여 카운터를 증가시켜 보세요
  • 패널을 다른 패널 위로 드래그하면 레이아웃이 자동으로 재구성됩니다
  • 레이아웃이 변경되어도 각 패널의 상태(카운터 값)는 유지됩니다 :::

3. 컴포넌트 API

DndGridContainer

그리드 시스템의 루트 컴포넌트입니다.

Prop타입기본값설명
widthnumber필수전체 그리드의 너비 (px)
heightnumber필수전체 그리드의 높이 (px)
childrenReactNode-DndGridSplit 또는 DndGridItem

DndGridSplit

영역을 두 개로 분할하는 컴포넌트입니다.

Prop타입기본값설명
direction'horizontal' | 'vertical'필수분할 방향 (horizontal: 상하, vertical: 좌우)
rationumber0.5첫 번째 자식(Primary)이 차지하는 비율 (0 ~ 1.0)
childrenReactNode-정확히 2개의 자식 컴포넌트가 필요합니다.

자식 순서:

  • vertical인 경우: 첫 번째 자식이 왼쪽, 두 번째 자식이 오른쪽
  • horizontal인 경우: 첫 번째 자식이 위쪽, 두 번째 자식이 아래쪽

DndGridItem

실제 콘텐츠를 담는 최소 단위(Leaf Node)입니다.

Prop타입설명
childrenReactNode화면에 표시될 실제 사용자 컴포넌트

ItemDrag

특정 영역이나 컨텐츠를 통해서만 드래그를 시작하고 싶을 때 사용합니다. 이 컴포넌트는 반드시 DndGridItem 내부에 선언되어야 합니다.

주요 활용 방법

방법 1: 아이템 전체를 드래그 영역으로 설정

ItemDragDndGridItemContent를 감싸면 아이템 내부 어디를 클릭해도 드래그가 가능해집니다.

tsx
<DndGridItem>
    <ItemDrag>
        <DndGridItemContent>
            <MyComponent />
        </DndGridItemContent>
    </ItemDrag>
</DndGridItem>

방법 2: 특정 버튼/영역을 드래그 핸들로 설정

직접적인 드래그 요소(핸들)를 제어하고 싶을 때 사용합니다. 특정 버튼만 ItemDrag로 감싸고, 실제 콘텐츠는 별도로 배치합니다.

tsx
<DndGridItem>
    <ItemDrag>
        <button>drag 버튼</button>
    </ItemDrag>
    <DndGridItemContent>
        <MyComponent />
    </DndGridItemContent>
</DndGridItem>

인터랙티브 요소 처리

자동 감지 기능

DndGrid는 버튼, 입력 필드, 링크 등의 인터랙티브 요소를 클릭할 때 자동으로 드래그를 시작하지 않습니다. 다음 요소들은 자동으로 감지됩니다:

표준 HTML 요소:

  • <button>, <input>, <textarea>, <select>, <a>, <label>
  • contenteditable 속성이 있는 요소

ARIA Roles:

  • role="button", role="link", role="textbox", role="combobox"
  • 전체 목록: button, link, textbox, combobox, listbox, searchbox, spinbutton, slider, switch, tab, menuitem, menuitemcheckbox, menuitemradio

기타:

  • tabindex >= 0 인 요소 (키보드 포커스 가능)

기본 사용법

표준 인터랙티브 요소는 추가 코드 없이 바로 작동합니다:

tsx
<DndGridItem>
    <ItemDrag className="h-full">
        <DndGridItemContent className="h-full">
            {/* ✅ 이 버튼들은 자동으로 작동합니다 */}
            <button onClick={handleClick}>클릭 가능</button>
            <input type="text" placeholder="입력 가능" />
            <a href="#">링크 클릭 가능</a>
        </DndGridItemContent>
    </ItemDrag>
</DndGridItem>

장점

이전에는 각 버튼마다 onMouseDown={(e) => e.stopPropagation()}을 작성해야 했지만, 이제는 자동으로 처리됩니다!

커스텀 인터랙티브 요소

ARIA role이 없는 커스텀 인터랙티브 요소는 자동으로 감지되지 않습니다. 두 가지 해결 방법이 있습니다:

방법 1: ARIA Role 추가 (권장)

접근성을 위해 적절한 ARIA role을 추가하세요:

tsx
<DndGridItem>
    <ItemDrag className="h-full">
        <DndGridItemContent className="h-full">
            {/* ✅ role="button"으로 자동 감지됨 */}
            <div
                role="button"
                onClick={handleClick}
                className="cursor-pointer"
            >
                커스텀 버튼
            </div>
        </DndGridItemContent>
    </ItemDrag>
</DndGridItem>

방법 2: stopPropagation 사용 (대안)

ARIA role을 추가할 수 없는 경우, 이벤트 전파를 수동으로 막을 수 있습니다:

tsx
<DndGridItem>
    <ItemDrag className="h-full">
        <DndGridItemContent className="h-full">
            {/* ⚠️ stopPropagation으로 수동 처리 */}
            <div
                onClick={handleClick}
                onMouseDown={(e) => e.stopPropagation()}
                className="cursor-pointer"
            >
                커스텀 버튼
            </div>
        </DndGridItemContent>
    </ItemDrag>
</DndGridItem>

주의

onMouseDown={(e) => e.stopPropagation()}은 접근성을 고려하지 않으므로 가능하면 **방법 1 (ARIA role 사용)**을 권장합니다.

ARIA role을 사용하면:

  • 스크린 리더 사용자가 요소의 역할을 이해할 수 있습니다
  • 키보드 내비게이션이 개선됩니다
  • 시맨틱 HTML을 준수하게 됩니다 :::

비활성 요소

disabled 속성이나 aria-disabled="true"가 설정된 요소는 인터랙티브로 간주되지 않으며, 드래그가 가능합니다:

tsx
{
    /* 비활성 버튼 위에서 드래그 가능 */
}
<button disabled>비활성 버튼</button>;

더 자세한 트러블슈팅은 DndGrid 트러블슈팅 가이드를 참조하세요.


DndGridItemContent

DragGridItem 내부에 있어야 하는 컴포넌트로 렌더링 될 ui를 감싸는 형태입니다.


4. 스타일링 및 커스터마이징

Drop Indicator 커스터마이징

DndGrid는 data-drop-quadrant attribute를 사용하여 드롭 위치를 표시합니다. 드래그 중인 아이템이 다른 아이템 위로 hover할 때, 자동으로 사분면(top, left, right, bottom)을 감지하여 시각적 피드백을 제공합니다.

기본 동작

기본적으로 드롭 가능한 영역에 box-shadow가 표시됩니다:

css
/* 기본 스타일 (자동 적용) */
.dnd-grid-item[data-drop-quadrant='top'] {
    box-shadow: inset 0 10px 10px -5px rgba(0, 0, 0, 0.3);
}

방법 1: CSS 오버라이드

글로벌 CSS 파일에서 기본 스타일을 오버라이드할 수 있습니다:

css
/* 상단 사분면 - 파란색 테두리로 변경 */
.dnd-grid-item[data-drop-quadrant='top'] {
    border-top: 3px solid #3b82f6 !important;
    box-shadow: none !important;
}

/* 좌측 사분면 - 그라디언트 배경 */
.dnd-grid-item[data-drop-quadrant='left'] {
    background: linear-gradient(
        to right,
        rgba(59, 130, 246, 0.2),
        transparent
    ) !important;
    box-shadow: none !important;
}

/* 우측 사분면 - 두꺼운 빨간 테두리 */
.dnd-grid-item[data-drop-quadrant='right'] {
    border-right: 4px solid #ef4444 !important;
    box-shadow: none !important;
}

/* 하단 사분면 - 점선 테두리 */
.dnd-grid-item[data-drop-quadrant='bottom'] {
    border-bottom: 3px dashed #f59e0b !important;
    box-shadow: none !important;
}

방법 2: dropIndicatorClassName Prop

특정 아이템에만 커스텀 스타일을 적용하려면 dropIndicatorClassName prop을 사용하세요:

tsx
<DndGridItem dropIndicatorClassName="custom-drop-style">
    <YourComponent />
</DndGridItem>
css
.custom-drop-style[data-drop-quadrant] {
    box-shadow: 0 0 15px rgba(34, 197, 94, 0.6);
    background: rgba(34, 197, 94, 0.1);
}

.custom-drop-style[data-drop-quadrant='top'] {
    border-top: 2px solid #22c55e;
}

사분면별 Attribute 값

사분면Attribute 값기본 스타일
상단data-drop-quadrant="top"inset 0 10px 10px -5px rgba(0, 0, 0, 0.3)
좌측data-drop-quadrant="left"inset 10px 0 10px -5px rgba(0, 0, 0, 0.3)
우측data-drop-quadrant="right"inset -10px 0 10px -5px rgba(0, 0, 0, 0.3)
하단data-drop-quadrant="bottom"inset 0 -10px 10px -5px rgba(0, 0, 0, 0.3)

애니메이션 추가

부드러운 transition이나 애니메이션을 추가할 수 있습니다:

css
.dnd-grid-item[data-drop-quadrant] {
    transition: all 200ms ease-in-out;
}

/* 펄스 효과 */
.dnd-grid-item[data-drop-quadrant='top'] {
    animation: pulse-top 1s ease-in-out infinite;
}

@keyframes pulse-top {
    0%,
    100% {
        box-shadow: inset 0 10px 10px -5px rgba(0, 0, 0, 0.3);
    }
    50% {
        box-shadow: inset 0 15px 15px -5px rgba(0, 0, 0, 0.5);
    }
}

Tailwind CSS 통합

Tailwind CSS의 data attribute variant를 사용할 수 있습니다:

tsx
<DndGridItem
    dropIndicatorClassName="
        data-[drop-quadrant=top]:border-t-4
        data-[drop-quadrant=top]:border-blue-500
        data-[drop-quadrant=left]:border-l-4
        data-[drop-quadrant=left]:border-green-500
        data-[drop-quadrant=right]:border-r-4
        data-[drop-quadrant=right]:border-red-500
        data-[drop-quadrant=bottom]:border-b-4
        data-[drop-quadrant=bottom]:border-orange-500
    "
>
    <YourComponent />
</DndGridItem>

접근성 고려사항

색상만으로 의존하지 말고 패턴이나 텍스처를 함께 사용하세요:

css
.dnd-grid-item[data-drop-quadrant='top'] {
    box-shadow: inset 0 10px 10px -5px rgba(0, 0, 0, 0.3);
    background-image: repeating-linear-gradient(
        45deg,
        transparent,
        transparent 10px,
        rgba(59, 130, 246, 0.1) 10px,
        rgba(59, 130, 246, 0.1) 20px
    );
}

고급 예제: 테마별 스타일

CSS Variables를 활용하면 테마별로 쉽게 스타일을 변경할 수 있습니다:

css
/* Light Theme */
[data-theme='light'] .dnd-grid-item[data-drop-quadrant] {
    --drop-indicator-color: rgba(0, 0, 0, 0.3);
}

/* Dark Theme */
[data-theme='dark'] .dnd-grid-item[data-drop-quadrant] {
    --drop-indicator-color: rgba(255, 255, 255, 0.4);
}

.dnd-grid-item[data-drop-quadrant='top'] {
    box-shadow: inset 0 10px 10px -5px var(--drop-indicator-color);
}

.dnd-grid-item[data-drop-quadrant='left'] {
    box-shadow: inset 10px 0 10px -5px var(--drop-indicator-color);
}

.dnd-grid-item[data-drop-quadrant='right'] {
    box-shadow: inset -10px 0 10px -5px var(--drop-indicator-color);
}

.dnd-grid-item[data-drop-quadrant='bottom'] {
    box-shadow: inset 0 -10px 10px -5px var(--drop-indicator-color);
}

DndGridItem Props

Prop타입기본값설명
childrenReactNode-화면에 표시될 실제 사용자 컴포넌트
classNamestringundefined아이템 컨테이너에 추가할 CSS 클래스
dropIndicatorClassNamestringundefineddrop indicator에 추가할 커스텀 CSS 클래스
allowDropbooleantrue이 아이템을 드롭 대상으로 허용할지 여부

5. 주요 특징 및 주의사항

상태 보존 (State Preservation)

DndGrid는 내부적으로 Flat Rendering 전략을 사용합니다. 드래그 앤 드롭으로 인해 트리의 깊이나 구조가 바뀌어도, DndGridItem 내부에 있는 사용자 컴포넌트는 리마운트(Unmount & Remount)되지 않고 상태E를 유지합니다.

Next.js (App Router) 지원

DndGrid는 클라이언트 측 인터랙션을 위해 브라우저 API와 상태 관리(Zustand)를 사용합니다. Next.js 환경에서 사용할 경우, 관련 컴포넌트를 사용하는 페이지나 부모 컴포넌트에 "use client" 지시문이 포함되어 있는지 확인하십시오.


6. 최적의 사용을 위한 가이드

  • 중첩 제한: 기술적으로는 무제한 중첩이 가능하지만, 사용자 경험과 성능을 위해 가급적 4단계 이하의 중첩을 권장합니다.
  • 고유한 콘텐츠: 각 DndGridItem에는 명확히 구분되는 콘텐츠를 배치하는 것이 가독성이 좋습니다.

트러블 슈팅

MIT 라이선스로 배포됩니다.