react-grid-layout이란?
react-grid-layout
은 React 애플리케이션에서 드래그 앤 드롭 및 크기 조절이 가능한 그리드 레이아웃을 손쉽게 구현할 수 있도록 도와주는 라이브러리입니다. 사용자가 직접 아이템의 위치나 크기를 변경할 수 있는 동적인 대시보드나 인터페이스를 만들 때 매우 유용합니다.
공식 문서와 예제가 잘 마련되어 있음에도 불구하고, 클래스형 컴포넌트 기반의 예시가 주를 이루고 타입스크립트 환경에서의 활용 예시가 부족하여 최신 React 프로젝트에 적용하는 데 어려움을 느끼는 경우가 있습니다. 이에 본 글에서는 react-grid-layout
을 타입스크립트와 함께 사용하며 얻은 실용적인 노하우를 공유하고자 합니다.
공식문서에는 구체적인 props 설명, 메모이제이션, 예제 등이 있으니, 포스팅을 읽으신 후에는 공식문서를 꼭 읽으시길 권유드립니다.
설치
npm install react-grid-layout
npm install -D @types/react-grid-layout
ReactGridLayout 컴포넌트 사용법
기본적으로 여러 사용방법이 있지만 가장 “React”스럽게 사용하는 법을 정리해보았습니다.
- 비제어 방식(Uncontrolled )
비제어 방식은 레이아웃을 따로 상태로 관리하지 않고 react-grid-layout
이 알아서 관리하게 합니다.
//MyGridComponent.tsx
import ReactGridLayout, { type Layout } from "react-grid-layout";
const initialLayouts: Layout[] = [
{ i: "1", x: 0, y: 0, w: 2, h: 2 },
{ i: "2", x: 2, y: 0, w: 2, h: 4 },
{ i: "3", x: 4, y: 0, w: 2, h: 2 },
];
export function MyGridComponent() {
return (
<ReactGridLayout
className="layout"
cols={12}
rowHeight={30}
width={1200}
>
{initialLayouts.map((item) => (
<div key={item.i} />
))}
</ReactGridLayout>
);
}
- 제어(Controlled) 방식
제어 방식은 레이아웃 상태를 개발자가 직접 useState
와 같은 React의 상태 관리 훅을 통해 관리하는 방식입니다. 제어 방식은 사용자의 레이아웃 변경 사항을 감지하여 특정 로직(예: 변경된 레이아웃 정보를 데이터베이스나 로컬 스토리지에 저장)을 수행하거나, 다른 외부 상태와 동기화해야 하는 경우에 매우 유용합니다..
//MyGridComponent.tsx
import ReactGridLayout, { type Layout } from "react-grid-layout";
import { useState } from "react";
const initialLayouts: Layout[] = [
{ i: "1", x: 0, y: 0, w: 2, h: 2 },
{ i: "2", x: 2, y: 0, w: 3, h: 4 },
{ i: "3", x: 5, y: 0, w: 2, h: 2 },
{ i: "4", x: 0, y: 2, w: 5, h: 2 },
];
export function MyGridComponent() {
const [layout, setLayout] = useState<Layout[]>(initialLayouts);
const onLayoutChange = (newLayout: Layout[]) => {
setLayout(newLayout);
console.log("Layout changed:", newLayout);
};
return (
<ReactGridLayout
className="layout"
layout={layout}
cols={12}
rowHeight={30}
width={1200}
onLayoutChange={onLayoutChange}
>
{initialLayouts.map((item) => (
<div key={item.i} />
))}
</ReactGridLayout>
);
}
스타일 적용하기
react-grid-layout
과 그 의존성인 react-resizable
은 기본적인 스타일을 제공합니다. 이 스타일을 적용해야 그리드와 아이템들이 올바르게 표시되고, 리사이즈 핸들 등이 나타납니다.
애플리케이션의 진입점(e.g., App.tsx
, index.tsx
)이나 해당 컴포넌트(MyGridComponent.tsx
) 등 적절한 곳에 아래 CSS 파일들을 불러옵니다.
// MyGridComponent.tsx 또는 App.tsx 등
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
커스텀 스타일 적용하기
기본 스타일을 기반으로 자신만의 스타일을 적용하고 싶다면, 다음 단계를 따를 수 있습니다.
node_modules/react-grid-layout/css/styles.css
와node_modules/react-resizable/css/styles.css
파일의 내용을 복사합니다.- 프로젝트 내의 새 CSS 파일(예:
custom-grid-styles.css
)에 붙여넣습니다. - 그리드 배경색, 아이템 스타일, 플레이스홀더 모양 등 원하는 부분을 자유롭게 수정합니다.
- 커스텀 CSS 파일을 불러옵니다.
/* custom-grid-styles.css */
.react-grid-layout {
position: relative;
transition: height 200ms ease;
background-color: #f0f0f0; /* 예시: 배경색 변경 */
}
.react-grid-item {
background-color: #ffffff; /* 예시: 아이템 배경색 변경 */
border: 1px solid #cccccc; /* 예시: 아이템 테두리 변경 */
}
.react-grid-item.react-grid-placeholder {
background: green; /* 예시: 드래그 중 플레이스홀더 색상 변경 */
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
/* ... 기타 react-resizable 스타일 등 ... */
// MyGridComponent.tsx 또는 App.tsx 등
import "./path/to/your/custom-grid-styles.css";
...
타입스크립트 응용하기
타입스크립트를 사용하면 Layout
객체를 확장하여 아이템에 추가적인 데이터를 포함시키고, 이를 타입 안전하게 관리할 수 있습니다.
// MyGridComponent.tsx
import ReactGridLayout, { type Layout } from "react-grid-layout";
import { useState } from "react";
interface MyLayoutItem extends Layout {
content?: string;
// 여기에 필요한 다른 커스텀 속성들을 추가할 수 있습니다.
// 예: type?: 'chart' | 'table' | 'text';
}
const initialItemsData: MyLayoutItem[] = [
{ i: "item-1", x: 0, y: 0, w: 2, h: 2, content: "아이템 1 (2x2)", isDraggable: true, isResizable: true },
{ i: "item-2", x: 2, y: 0, w: 3, h: 4, content: "아이템 2 (3x4)", isDraggable: true, isResizable: true },
{ i: "item-3", x: 5, y: 0, w: 2, h: 2, content: "아이템 3 (고정)", static: true }, // static: true는 드래그/리사이즈 불가
{ i: "item-4", x: 0, y: 2, w: 5, h: 2, content: "아이템 4 (내용만)" },
];
export function MyGridComponent()
const [items, setItems] = useState<MyLayoutItem[]>(initialItemsData);
const onLayoutChange = (newLayout: Layout[]) => {
console.log("Layout changed:", newLayout);
// 중요: newLayout은 순수한 Layout[] 타입입니다.
// 기존 items의 추가 데이터(content 등)와 병합해야 합니다.
setItems(prevItems =>
prevItems.map(item => {
const layoutItem = newLayout.find(l => l.i === item.i);
return layoutItem ? { ...item, ...layoutItem } : item;
})
);
};
return (
<ReactGridLayout
className="layout"
layout={items}
cols={12}
rowHeight={30}
width={1200}
onLayoutChange={onLayoutChange}
>
{items.map((item) => (
<div key={item.i}>
<span className="text">{item?.content || item.i}</span>
{/* 여기에 각 아이템의 실제 컨텐츠를 렌더링 (예: 차트, 테이블 등) */}
</div>
))}
</ReactGridLayout>
);
}
Responsive컴포넌트 사용법
뷰포트에 따라 반응형으로 레이아웃을 구현하고 싶다면 다음과 같이 사용할 수 있습니다.
import {
Responsive,
WidthProvider,
type Layout,
type Layouts,
} from "react-grid-layout";
import { useState } from "react";
const ResponsiveGridLayout = WidthProvider(Responsive);
interface MyResponsiveItem extends Layout {
content?: string;
}
const initialLayouts: Layouts = {
lg: [
{ i: "resp-1", x: 0, y: 0, w: 4, h: 2 },
{ i: "resp-2", x: 4, y: 0, w: 4, h: 2 },
{ i: "resp-3", x: 8, y: 0, w: 4, h: 2 },
],
md: [
{ i: "resp-1", x: 0, y: 0, w: 6, h: 2 },
{ i: "resp-2", x: 6, y: 0, w: 6, h: 2 },
{ i: "resp-3", x: 0, y: 2, w: 12, h: 2 },
],
sm: [
{ i: "resp-1", x: 0, y: 0, w: 12, h: 2 },
{ i: "resp-2", x: 0, y: 2, w: 12, h: 2 },
{ i: "resp-3", x: 0, y: 4, w: 12, h: 2 },
],
};
const initialItems: MyResponsiveItem[] = [
{ i: "resp-1", x: 0, y: 0, w: 0, h: 0, content: "Item 1 Content" }, // x,y,w,h는 layouts에서 오버라이드됨
{ i: "resp-2", x: 0, y: 0, w: 0, h: 0, content: "Item 2 Content" },
{ i: "resp-3", x: 0, y: 0, w: 0, h: 0, content: "Item 3 Content" },
];
export function MyGridComponent() {
const [layouts, setLayouts] = useState<Layouts>(initialLayouts);
const [itemsData, setItemsData] = useState<MyResponsiveItem[]>(initialItems);
const onLayoutChange = (currentLayout: Layout[], allLayouts: Layouts) => {
console.log("Current Layout (for current breakpoint):", currentLayout);
console.log("All Layouts:", allLayouts);
setLayouts(allLayouts);
};
const onBreakpointChange = (newBreakpoint: string, newCols: number) => {
console.log("Breakpoint changed to:", newBreakpoint, "Cols:", newCols);
};
return (
<ResponsiveGridLayout
className="layout"
layouts={layouts}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
rowHeight={30}
onLayoutChange={onLayoutChange}
onBreakpointChange={onBreakpointChange}
>
{itemsData.map((item) => (
<div
key={item.i}
style={{ border: "1px solid red", backgroundColor: "lightblue" }}
>
<span className="text">
{item.content || `Content for ${item.i}`}
</span>
<p>ID: {item.i}</p>
</div>
))}
</ResponsiveGridLayout>
);
}
WidthProvider
HOC(Higher Order Component)로 감싸면 부모 컨테이너의 너비를 자동으로 감지하여width
prop을 전달해줍니다.layouts
prop은{ [breakpoint: string]: Layout[] }
형태의 객체를 받습니다.breakpoints
와cols
props를 통해 각 화면 크기 구간과 해당 구간에서의 컬럼 수를 정의합니다.onLayoutChange
는 현재 브레이크포인트의currentLayout
과 모든 브레이크포인트의allLayouts
를 인자로 받습니다.- 라이브러리에서 제공하는
WidthProvider
는resize
이벤트 감지에 디바운싱을 적용하지 않고 있습니다. 따라서, 성능 최적화를 고려한다면 커스텀해서 사용하는 것을 추천합니다.