Validation
그리드는 검증 정책을 갖지 않습니다 — 소비자가 두 시점(동기 commit-전 / 비동기 서버)에서 합성합니다. 동기 검증은 같은 파서를 TextEditor.parse(타이핑)와 meta.parseValue(붙여넣기·비우기)에 함께 넘겨 일치시킵니다.
그리드는 검증 정책을 갖지 않는다
이 그리드는 헤드리스입니다 — "무엇이 유효한 값인가"에 대해 어떤 의견도 갖지 않습니다. 검증은 전부 소비자 코드이며, 그리드가 가진 두 시점에 끼워 넣습니다.
| 시점 | 무엇 | 결과 |
|---|---|---|
| 동기 (commit 전) | 에디터가 commit하기 전에 입력을 검사 | 무효면 commit 안 함 + 편집 유지 + aria-invalid |
| 비동기 (서버) | onCellEdit가 반환한 Promise가 서버 응답으로 판정 | reject면 saveStatus="error"로 셀 강조 |
이 둘은 배타적이 아니라 합성됩니다. 동기로 형식을 거르고, 통과한 값만 비동기로 서버에 확인하는 식입니다.
한 파서를 두 동기 경로에 — meta.parseValue + TextEditor.parse
그리드가 raw 문자열을 컬럼 값으로 바꾸는 경로 — 붙여넣기·잘라내기·비우기(clear) — 는 항상 column.meta.parseValue를
거칩니다. (타이핑은 <DataGrid.TextEditor parse>가, 자동 채우기(fill)는 meta.fill이 처리하므로 이 경로가 아닙니다.)
parseValue가 throw하면 그 값은 무효로 간주되어 해당 셀만 건너뜁니다(다른 셀은 정상 반영). 같은 검증을 타이핑에도
적용하려면 아래처럼 같은 파서를 TextEditor의 parse에도 넘깁니다(정의는 한 곳, 두 경로에 연결).
아래 그리드에서 나이 셀을 더블클릭해 999나 abc를 입력하면 commit되지 않고 편집이 유지되며 aria-invalid가 붙습니다.
0~150 범위의 숫자면 정상 저장됩니다. 같은 parseAge가 meta.parseValue(붙여넣기·비우기)와 TextEditor.parse(타이핑)에 함께 연결됩니다.
// 검증 파서 — throw하면 무효. 붙여넣기·잘라내기·비우기가 이 규칙을 타고, 같은 파서를 TextEditor.parse에 넘기면 타이핑도 일치. const parseAge = (raw) => { const n = Number(raw.trim()); if (raw.trim() === '' || Number.isNaN(n) || n < 0 || n > 150) throw new Error('0~150 숫자만'); return n; }; const initialData = [ { id: '1', name: '김민준', age: 27 }, { id: '2', name: '이서연', age: 31 }, { id: '3', name: '박도윤', age: 24 }, { id: '4', name: '최하은', age: 38 }, { id: '5', name: '정시우', age: 29 }, ]; const columns = [ { accessorKey: 'name', header: ({ header }) => <DataGrid.HeaderCell header={header}>채널</DataGrid.HeaderCell>, cell: (info) => <DataGrid.Cell cell={info.cell}>{info.getValue()}</DataGrid.Cell>, }, { accessorKey: 'age', meta: { editable: true, parseValue: parseAge, // ← 붙여넣기·잘라내기·비우기 검증 }, header: ({ header }) => <DataGrid.HeaderCell header={header}>나이</DataGrid.HeaderCell>, cell: (info) => ( <DataGrid.Cell cell={info.cell}> <DataGrid.CellDisplay>{String(info.getValue() ?? '')}</DataGrid.CellDisplay> <DataGrid.CellEditor> {/* 인라인 타이핑 검증 — 같은 파서를 넘겨 한 곳에서만 정의합니다(정합성). */} <DataGrid.TextEditor parse={parseAge} /> </DataGrid.CellEditor> </DataGrid.Cell> ), }, ]; function ValidatedGrid() { const [data, setData] = useState(initialData); const table = useDataGrid({ data, columns, getCoreRowModel: getCoreRowModel(), getRowId: (row) => row.id, }); const selection = useDataGridSelection(table); const editing = useDataGridEditing(table, { selection, onCellEdit: ({ coord, newValue }) => setData((rows) => rows.map((row) => (row.id === coord.rowId ? { ...row, [coord.columnId]: newValue } : row))), }); return ( <DataGrid.Root table={table} selection={selection} editing={editing} size="md"> <DataGrid.Header /> <DataGrid.Body /> </DataGrid.Root> ); } render(<ValidatedGrid />);
검증을 <DataGrid.TextEditor parse>에만 두면 타이핑만 검증됩니다 — 붙여넣기·비우기로 들어온 값은
건너뜁니다. 두 경로를 일치시키려면 같은 파서를 meta.parseValue에도 넘기세요. 그러면 타이핑·붙여넣기·잘라내기·비우기가
모두 동일한 규칙을 탑니다. 단, 자동 채우기(fill)는 meta.fill이 값을 만들어 parseValue를 거치지 않으므로, fill 검증이
필요하면 fill 값 생성 단계에서 보장하세요.
동기 검증 1 — <DataGrid.TextEditor parse> (테두리만)
가장 가벼운 형태입니다. 패키지 기본 <DataGrid.TextEditor>의 parse가 throw하면 그리드는 commit하지 않고
편집을 유지하며, 에디터 입력에 aria-invalid="true"를 부여합니다(재입력하면 해제). 메시지 UI 없이 "무효 테두리만"
필요할 때 충분합니다.
function parseEmail(raw: string): string {
const v = raw.trim();
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v)) throw new Error('이메일 형식 오류');
return v;
}
<DataGrid.CellEditor>
<DataGrid.TextEditor parse={parseEmail} />
</DataGrid.CellEditor>- 무효면: 편집 유지 ·
aria-invalid="true"· commit·이동 없음. - 유효면:
commit+moveTo(Enter=아래, Tab=오른쪽)로 이동.
동기 검증 2 — 커스텀 에디터 + 인라인 메시지 (useDataGridCellEdit)
"왜 무효인지" 메시지나 인라인 배지가 필요하면 헤드리스 탈출구인
useDataGridCellEdit으로 에디터를 직접 그립니다. commit 전에 검증해 무효면 commit을 호출하지 않고 메시지를 띄웁니다.
function ValidatedCell({ info, validate }) {
const edit = useDataGridCellEdit(info.cell);
const [error, setError] = React.useState(null);
const tryCommit = (value, move) => {
const err = validate(value); // null이면 유효, string이면 메시지
if (err) { setError(err); return false; } // 편집 유지, commit 안 함
setError(null);
edit.commit(value, move);
return true;
};
return (
<DataGrid.Cell cell={info.cell}>
{edit.isEditing ? (
<>
<input
autoFocus
defaultValue={edit.prefillChar ?? String(edit.initialValue ?? '')}
aria-invalid={error ? true : undefined}
onChange={() => error && setError(null)}
onBlur={(e) => { if (!tryCommit(e.currentTarget.value)) edit.cancel(); }}
onKeyDown={(e) => {
// IME 조합 확정 키는 commit 라우팅에서 제외(조합 Enter 누수 방지).
if (e.nativeEvent.isComposing || e.key === 'Process' || e.nativeEvent.keyCode === 229) return;
if (e.key === 'Enter') { e.preventDefault(); tryCommit(e.currentTarget.value, 'down'); }
else if (e.key === 'Escape') { e.preventDefault(); edit.cancel(); }
}}
/>
{error && <InlineErrorBadge message={error} />}
</>
) : (
String(info.getValue() ?? '')
)}
</DataGrid.Cell>
);
}인라인 메시지 배지는 행 높이에 영향을 주지 않도록 position: absolute로 띄우는 것을 권장합니다(레이아웃 shifting
방지). 커스텀 에디터에서도 위 IME 조합 가드는 반드시 두세요 — 패키지 TextEditor와 동일한 함정 방지책입니다.
비동기 / 서버 검증 — onCellEdit reject → saveStatus
형식은 맞지만 서버가 거절할 수 있는 값(중복 이메일 등)은 비동기로 판정합니다. onCellEdit가 반환한 Promise가
reject되면 그 셀의 saveStatus가 "error"가 되어 강조됩니다(낙관적 반영 후 실패 롤백/표시는 소비자 정책).
const editing = useDataGridEditing(grid, {
selection,
onCellEdit: async ({ coord, newValue }) => {
const res = await api.save(coord, newValue);
if (!res.ok) throw new Error('서버 거절'); // reject → saveStatus='error'
setData(/* 반영 */);
},
});saveStatus는 useDataGridCellEdit로 셀에서 읽어 스피너/에러 테두리를 그릴 수 있습니다. 자세한 비동기 편집
라이프사이클은 Storybook의 Async Editing 스토리를 참고하세요.
비동기 검증은 반환 Promise를 reject하세요 — onCellEdit 안에서 동기 throw 하는 것은 잘못된 사용입니다
(검증 실패는 commit 전 동기 검증 또는 async reject가 맞습니다). 다만 실수로 동기 throw해도 그리드는 견딥니다(아래).
견고성 계약 — 소비자 콜백의 동기 throw는 안전 강등된다
onCellEdit·parseValue·equals는 전부 소비자 코드입니다. 이들이 키/붙여넣기 이벤트 핸들러 안에서 동기적으로
throw하면, 순진한 구현에서는 예외가 핸들러 밖으로 새어 호스트 앱을 크래시시킵니다. 이 그리드는 그러지 않습니다:
- 예외를 격리해 그 셀만 실패로 떨어뜨리고(나머지 작업·이전 성공 편집의 undo는 유지),
- dev 빌드에서
console.warn1회로 원인을 알리며(반복 throw도 1회만), - 호스트 앱은 크래시하지 않습니다.
이는 "올바른 사용을 강제"하는 게 아니라, 실수가 앱 전체를 무너뜨리지 않게 하는 방어 계약입니다. 검증은 위의 동기/비동기 시점에 두고, 견고성은 그리드가 보장합니다.
다음 단계
- Editing — 편집 라이프사이클 ·
meta.editable· trigger/commit · record-on-commit/confirm · undo/redo. - Clipboard & Fill — 붙여넣기·잘라내기는
meta.parseValue검증을 타고, 자동 채우기는meta.fill로 값을 만듭니다. - Extending —
onCellKeyDownveto 등 모든 override seam. - API reference —
meta.parseValue·UseDataGridEditingOptions공개 표.