Featuring Design System
Selection & Editing

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이 처리하므로 이 경로가 아닙니다.) parseValuethrow하면 그 값은 무효로 간주되어 해당 셀만 건너뜁니다(다른 셀은 정상 반영). 같은 검증을 타이핑에도 적용하려면 아래처럼 같은 파서를 TextEditorparse에도 넘깁니다(정의는 한 곳, 두 경로에 연결).

아래 그리드에서 나이 셀을 더블클릭해 999abc를 입력하면 commit되지 않고 편집이 유지되며 aria-invalid가 붙습니다. 0~150 범위의 숫자면 정상 저장됩니다. 같은 parseAgemeta.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>parsethrow하면 그리드는 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(/* 반영 */);
  },
});

saveStatususeDataGridCellEdit로 셀에서 읽어 스피너/에러 테두리를 그릴 수 있습니다. 자세한 비동기 편집 라이프사이클은 Storybook의 Async Editing 스토리를 참고하세요.

비동기 검증은 반환 Promise를 reject하세요 — onCellEdit 안에서 동기 throw 하는 것은 잘못된 사용입니다 (검증 실패는 commit 전 동기 검증 또는 async reject가 맞습니다). 다만 실수로 동기 throw해도 그리드는 견딥니다(아래).

견고성 계약 — 소비자 콜백의 동기 throw는 안전 강등된다

onCellEdit·parseValue·equals는 전부 소비자 코드입니다. 이들이 키/붙여넣기 이벤트 핸들러 안에서 동기적으로 throw하면, 순진한 구현에서는 예외가 핸들러 밖으로 새어 호스트 앱을 크래시시킵니다. 이 그리드는 그러지 않습니다:

  • 예외를 격리해 그 셀만 실패로 떨어뜨리고(나머지 작업·이전 성공 편집의 undo는 유지),
  • dev 빌드에서 console.warn 1회로 원인을 알리며(반복 throw도 1회만),
  • 호스트 앱은 크래시하지 않습니다.

이는 "올바른 사용을 강제"하는 게 아니라, 실수가 앱 전체를 무너뜨리지 않게 하는 방어 계약입니다. 검증은 위의 동기/비동기 시점에 두고, 견고성은 그리드가 보장합니다.

다음 단계

  • Editing — 편집 라이프사이클 · meta.editable · trigger/commit · record-on-commit/confirm · undo/redo.
  • Clipboard & Fill — 붙여넣기·잘라내기는 meta.parseValue 검증을 타고, 자동 채우기는 meta.fill로 값을 만듭니다.
  • ExtendingonCellKeyDown veto 등 모든 override seam.
  • API referencemeta.parseValue · UseDataGridEditingOptions 공개 표.