정렬·필터·검색·페이징·편집을 헤드리스 코어에서 처리하는 멀티 프레임워크 데이터 그리드 라이브러리입니다. React, Vue 3, Vue 2.7, Svelte 5, 순수 JS CDN을 지원하며 Set 필터·그룹화·클립보드 등 유료급 기능을 MIT로 무료 제공합니다.
React·Vue·Svelte·순수 JS 어디서든 똑같이 동작하는, 헤드리스 데이터 그리드 라이브러리.
moda-grid는 정렬·필터·검색·페이징·편집 같은 모든 데이터 로직을 순수 TypeScript 코어(GridCore)에 두고, 각 프레임워크 어댑터는 얇은 렌더링 레이어만 담당합니다. 프레임워크가 달라도 기능 동작은 항상 같습니다.
Set 필터, 그룹화+집계, 클립보드, 범위 선택처럼 다른 그리드에서는 유료(Enterprise)인 기능을 MIT로 무료 제공합니다.
CDN에서 불러온 실제 코어로 동작합니다. 정렬·다중 정렬·연산자/Set 필터·검색·페이징·가상 스크롤·셀/범위/행 선택·인라인 편집·Undo/Redo· 클립보드 복사/붙여넣기·컬럼 리사이즈/재배치/고정/숨김·행 그룹화와 집계·고정 행·총계·상태 저장/복원·행 드래그·행 상태 추적(I/U/D 마킹·변경분 저장)을 모두 써볼 수 있고, 트리 데이터와 서버 사이드 무한 스크롤 모드도 탭으로 전환해 볼 수 있습니다. 그리드 아래 패널에는 지금 설정과 방금 실행한 API 호출을 그대로 재현하는 코드가 실시간으로 표시됩니다.
셀 더블클릭 또는 문자 입력으로 편집 · 드래그로 범위 선택 · ⌘/Ctrl+C·V 복사/붙여넣기 · ⌘/Ctrl+Z·Y 되돌리기 · 방향키/Shift+방향키 이동과 확장 · ⠿ 핸들로 행 드래그 · 헤더 우측 경계로 너비 조절 · 헤더 드래그로 순서 변경
| 사용 환경 | 패키지 | 비고 |
|---|---|---|
| React | @moda-grid/react | DataGrid 컴포넌트 + useGridCore 훅, renderCell/renderEditor 확장 |
| Vue 3 | @moda-grid/vue | useGrid + DataGrid, 컬럼별 커스텀 셀·편집기 슬롯 |
| Vue 2.7 | @moda-grid/vue2 | Vue 3과 동일한 Composition API 사용법 |
| Svelte 5 | @moda-grid/svelte | runes 기반 createGridStore + DataGrid |
| 순수 JS / CDN | moda-grid.js | 빌드 도구 없이 <script> 한 줄, window.ModaGrid |
| 프레임워크 없음 | @moda-grid/core | 헤드리스 코어 GridCore 직접 사용 |
npm i @moda-grid/react # Reactnpm i @moda-grid/vue # Vue 3npm i @moda-grid/vue2 # Vue 2.7npm i @moda-grid/svelte # Svelte 5npm i @moda-grid/core # 헤드리스 코어만https://grid.modaolive.com/moda-grid.jshttps://grid.modaolive.com/style.cssnpm i @moda-grid/react처럼 어댑터를 설치하거나, CDN <script> 한 줄로 코어를 로드합니다.
columns에 field·header만 적어도 동작합니다. filterable·cellEditor 등은 필요할 때 추가합니다.
어댑터는 <DataGrid> 컴포넌트 하나면 끝. 코어 직접 사용 시 subscribe + getSnapshot으로 연결합니다.
// npm i @moda-grid/react
import { DataGrid, useGridCore } from "@moda-grid/react";
import "@moda-grid/react/styles.css";
const columns = [
{ field: "id", header: "ID", width: 60 },
{ field: "name", header: "이름", filterable: true },
{ field: "role", header: "역할",
renderCell: (value) => <span className={`badge role-${value}`}>{value}</span> },
];
function App() {
const { grid, snapshot } = useGridCore({ columns, data: users });
return (
<>
<input onChange={(e) => grid.setSearch(e.target.value)} />
<button onClick={() => grid.exportToCsv({ filename: "users.csv" })}>
CSV보내기
</button>
<DataGrid grid={grid} columns={columns}
height={480} rowHeight={37}
selectionMode="multi-cell" columnController />
</>
);
}모든 뮤테이션은 내부에서 스냅샷을 재계산하고 구독자에게 알립니다. 스냅샷은 메모이즈되어 React useSyncExternalStore와도 바로 맞습니다.
어댑터는 대부분 prop·옵션 한 줄로 켜지고, 코어만 쓸 때는 API를 직접 호출합니다. 라이브 데모의 각 컨트롤에 마우스를 올리면 대응 API가 툴팁으로 표시됩니다.
| 기능 | 켜는 방법 (옵션/prop/입력) | 코어 API |
|---|---|---|
| 정렬 | 기본 on — 헤더 클릭 | toggleSort(field) / setSort(field, "asc") |
| 다중 정렬 | Shift + 헤더 클릭 | toggleSort(field, true) |
| 컬럼 필터 | ColumnDef.filterable: true + filterType: "text"|"number"|"date" | setFilter(field, { operator, value, valueTo? }) |
| Set 필터 (엑셀식) | filterType: "set" | setFilter(field, { operator: "set", values: [...] }) |
| 전역 검색 | 외부 input 연결 | setSearch(text) |
| 페이징 | GridOptions.pageSize | setPage(pageIndex, pageSize?) |
| 기능 | 켜는 방법 (옵션/prop/입력) | 코어 API |
|---|---|---|
| 셀 선택 모드 | selectionMode: "single-cell"|"multi-cell"|"row" | setSelectionMode(mode) |
| 활성 셀·범위 | 클릭 / 드래그 / 방향키·Shift+방향키 | setActiveCell(r,c) / setCellRange / navigateCell |
| 행 선택 + 체크박스 | rowCheckboxes prop | toggleRowSelection(id) / toggleAllRows() |
| 인라인 편집 | editable + cellEditor: "text"|"number"|"select"|"date"|"custom" | startEditing / updateEditValue / commitEditing / cancelEditing |
| 입력 검증 | ColumnDef.validate(value, row) | 실패 시 snapshot.editError |
| 실행 취소·다시 실행 | 자동 기록 (undoLimit 기본 100) | undo() / redo() / canUndo() |
| 복사·붙여넣기 (TSV) | 어댑터에서 ⌘/Ctrl+C·V 자동 | getSelectionTsv() / pasteTsv(tsv) |
| 선택 집계 상태바 | statusBar prop (기본 on) | snapshot.selectionAggregates |
| 찾기·바꾸기 | — | findCells / findNext / replaceAll(find, repl) |
| 셀·헤더 우클릭 메뉴 | contextMenu / headerContextMenu 옵션 (프리셋: defaultContextMenuItems) | openContextMenu / runContextMenuItem / snapshot.contextMenu |
| 기능 | 켜는 방법 (옵션/prop/입력) | 코어 API |
|---|---|---|
| 행 추가 | — | addRows(rows) / importCsv(csv) → I 마킹 |
| 수정 추적 | 편집·pasteTsv 시 자동 마킹 | → U 상태 (I 행은 수정해도 I 유지) |
| 삭제 마킹·복원 | — | deleteRowsByIds(ids) → D / restoreRowsByIds(ids) |
| 상태 컬럼 표시 | rowStatus prop — 맨 왼쪽 상태 컬럼 | snapshot.rowStates + mg-row-I/U/D CSS 클래스 |
| 변경분 저장·취소 | — | validateChanges() → getChanges() → commitChanges() / clearChanges() |
| 기능 | 켜는 방법 (옵션/prop/입력) | 코어 API |
|---|---|---|
| 컬럼 리사이즈 | resizable (기본 on) — 헤더 경계 드래그 | setColumnWidth(field, px) |
| 자동 너비 | 경계 더블클릭 | autoSizeColumn / autoSizeAllColumns |
| 컬럼 재배치 | reorderable (기본 on) — 헤더 드래그 | reorderColumn(from, to) |
| 컬럼 고정 | ColumnDef.pinned: "left"|"right" | setColumnPinned(field, pinned) |
| 컬럼 표시·숨김 | columnController prop (컬럼 ▾ 팝오버) | setColumnVisible / setAllColumnsVisible |
| 다단계 그룹 헤더 | ColumnDef.group + GridOptions.columnGroups | snapshot.headerGroups |
| 레이아웃 초기화 | — | resetColumnLayout() |
| 행 드래그앤드롭 | ColumnDef.rowDrag: true | begin/update/endRowDrag · moveRow |
| 고정 행 | pinnedTopRows / pinnedBottomRows 옵션 | setPinnedTopRows / setPinnedBottomRows |
| 전체 총계 (tfoot) | ColumnDef.aggregationFn: "sum"|"avg"|"min"|"max"|"count" | snapshot.grandTotals |
| 셀 병합 | ColumnDef.merge: "row"|"col"|"both" | getCellSpan(row, col) → { rowSpan, colSpan, hidden } |
| 상태 저장·복원 | — | getState() / applyState(saved) |
| 테마 | styles.css의 --grid-* 변수 / .grid-theme-dark | cellClass / rowClass 조건부 클래스 |
| 기능 | 켜는 방법 (옵션/prop/입력) | 코어 API |
|---|---|---|
| 가상 스크롤 | 어댑터: height+rowHeight props | virtualScroll 옵션 + handleScroll(scrollTop) |
| 행 그룹화 + 집계 | — | setGroupBy(["dept"]) + ColumnDef.aggregationFn |
| 피벗 테이블 | pivot prop / 옵션 — 행·열 디멘션+측정값 | setPivot({ rows, columns?, values: [{field, agg}] }) / isPivot() |
| 트리 데이터 | treeData prop | treeData: { childrenKey } 또는 { getParentId } |
| 서버 사이드 무한 스크롤 | serverSide prop | serverSide: { dataSource, cacheBlockSize } / refreshServerRows |
| CSV보내기 | — | exportToCsv({ filename, visibleColumnsOnly, selectedRowsOnly }) |
| 엑셀·PDF 내보내기 | — | exportToXlsx({ filename, styled }) / exportToPdf / print() |
복사해서 바로 쓸 수 있는 코드 조각입니다.
컬럼 고유값을 체크리스트로 필터링 — 엑셀 필터와 같은 UX
// 컬럼에 filterType 지정
{ field: "role", header: "역할", filterable: true, filterType: "set" }
grid.getUniqueValues("role"); // ["admin","editor","viewer","guest"]
grid.setFilter("role", { operator: "set", values: ["admin","editor"] });
grid.setFilter("role", null); // 전체 선택 = 필터 해제현재 표시 상태(필터·정렬·펼침) 그대로 내보내기. UTF-8 BOM 포함이라 엑셀에서 한글 깨짐 없음
grid.exportToCsv({
filename: "users.csv",
visibleColumnsOnly: true, // 숨긴 컬럼 제외 (기본값)
selectedRowsOnly: false, // 선택한 행만 내보낼 때 true
});추가·수정·삭제를 마킹 추적하고 변경분만 전송. rowStatus prop으로 왼쪽에 상태 컬럼 표시
// I = addRows/importCsv · U = 편집/pasteTsv · D = deleteRowsByIds (I행은 즉시 제거)
grid.addRows([{ id: "new-1", name: "새 사원" }]);
grid.deleteRowsByIds(["3", "7"]); // D 마킹 — commit 전까지 화면에 남음
grid.restoreRowsByIds(["3"]); // D 마킹 해제
async function save() {
const errors = grid.validateChanges(); // I/U 행 required/validate 검사
if (errors.length) return console.warn(errors);
await fetch("/api/users", {
method: "POST",
body: JSON.stringify(grid.getChanges()), // { inserted, updated, deleted }
});
grid.commitChanges(); // D 제거 + 마킹 초기화
}
// 마킹만 취소(화면 복귀)는 grid.clearChanges()정렬·필터·검색·페이징·그룹화·컬럼 레이아웃을 localStorage에
// 저장 — 직렬화 가능한 객체가 반환됨
localStorage.setItem("grid", JSON.stringify(grid.getState()));
// 복원 — 없는 컬럼을 참조해도 무시되므로 스키마 변경에 안전
grid.applyState(JSON.parse(localStorage.getItem("grid")!));
// 부분 적용도 가능
grid.applyState({ sortState: saved.sortState });뷰포트가 미로드 블록에 닿으면 getRows 자동 호출. 정렬·필터는 서버 파라미터로 전달
const grid = new GridCore({
columns,
data: [], // 서버 모드에서는 무시
serverSide: {
cacheBlockSize: 50,
dataSource: {
async getRows({ startRow, endRow, sortModel, filterModel }) {
const res = await fetch(`/api/rows?start=${startRow}&end=${endRow}`);
return res.json(); // { rows, lastRowIndex? }
},
},
},
virtualScroll: { rowHeight: 37, viewportHeight: 480 },
});
grid.refreshServerRows(); // 캐시 폐기 + 재요청부모-자식 계층을 펼침/접힘 트리로. 두 가지 데이터 형태 지원
// nested — 행 안에 자식 배열
new GridCore({ columns, data, treeData: { childrenKey: "children" } });
// flat — parentId로 연결 (부모가 필터링된 행은 루트로 승격)
new GridCore({ columns, data,
getRowId: (r) => r.id,
treeData: { getParentId: (r) => r.parentId },
});
grid.toggleTreeExpanded(rowId); // 펼침/접힘어댑터의 ColumnDef 확장으로 JSX 렌더러와 에디터를 컬럼에 직접 지정
{
field: "role",
header: "역할",
renderCell: (value) => <span className={`badge role-${value}`}>{value}</span>,
renderEditor: (ctx) => (
<select value={ctx.value} onChange={(e) => ctx.setValue(e.target.value)} />
),
}
// Vue는 #cell-{field} / #editor-{field} 슬롯, Svelte는 cell/editor snippet필드로 그룹화하고 aggregationFn 컬럼의 합계·평균을 그룹 행에 표시
{ field: "age", aggregationFn: "avg" } // 집계 대상 지정
grid.setGroupBy(["dept", "role"]); // 다단계 그룹화
grid.expandAllGroups(); // 모두 펼치기
grid.collapseAllGroups(); // 모두 접기
// snapshot.grandTotals에 전체 총계도 자동 계산표시 문자열 변환과 값 추출은 컬럼 옵션으로
{
field: "salary",
header: "연봉",
valueGetter: (row) => row.pay.annual, // 중첩 값 추출
formatter: (v) => `${Number(v).toLocaleString()}원`, // 표시 변환
cellClass: (p) => (p.value > 5000 ? "text-red-500" : ""), // 조건부 클래스
}Set 필터·그룹화·집계·클립보드·범위 선택·피벗 같은 기능은 AG Grid에서는 Enterprise(유료) 전용입니다. moda-grid는 MIT 라이선스로 전부 무료입니다.