@mycrm-ui/react-table
React 19 / React 18에서 사용 가능한 고성능 테이블 컴포넌트입니다.
@mycrm-ui/react-table은 헤드리스(Headless) 라이브러리입니다. 소스코드를 그대로 사용하면 스타일이 적용되지 않은 상태로 렌더링됩니다. 문서의 미리보기는 이해를 돕기 위해 별도 스타일을 적용한 예시입니다.
columns, data, rowKey만으로 기본 테이블을 렌더링합니다.
| 이름 | 이메일 | 역할 |
|---|---|---|
| 홍길동 | hong@example.com | 관리자 |
| 김철수 | kim@example.com | 사용자 |
| 이영희 | lee@example.com | 사용자 |
단일 정렬과 멀티 정렬을 모두 지원합니다.
단일 정렬
| 이름 | 이메일 | 역할 | 나이 |
|---|---|---|---|
| 김철수 | kim@example.com | 사용자 | 28 |
| 홍길동 | hong@example.com | 관리자 | 35 |
| 이영희 | lee@example.com | 사용자 | 42 |
멀티 정렬
Shift를 누른 채 컬럼 헤더를 클릭하면 순서대로 멀티 정렬이 적용됩니다.
| 카테고리1 | 브랜드 | 가격2 |
|---|---|---|
| 노트북 | 삼성 | 1,500,000원 |
| 노트북 | LG | 1,500,000원 |
| 노트북 | 삼성 | 1,200,000원 |
| 모니터 | LG | 450,000원 |
| 모니터 | 삼성 | 450,000원 |
| 모니터 | LG | 320,000원 |
selection 옵션으로 체크박스 행 선택을 활성화합니다. 헤더의 체크박스로 전체 선택/해제가 가능합니다.
| Key | 이름 | 이메일 | 역할 | |
|---|---|---|---|---|
| 1 | 홍길동 | hong@example.com | 관리자 | |
| 2 | 김철수 | kim@example.com | 사용자 | |
| 3 | 이영희 | lee@example.com | 사용자 | |
| 4 | 박민수 | park@example.com | 편집자 | |
| 5 | 최지은 | choi@example.com | 사용자 |
selectedKeys
[]컬럼 단위 필터를 제공합니다. filterType으로 텍스트, 셀렉트, 날짜 범위, 숫자 범위 필터를 선택할 수 있습니다.
| 이름 | 이메일 | 역할 | |
|---|---|---|---|
| 홍길동 | hong@example.com | 관리자 | |
| 김철수 | kim@example.com | 사용자 | |
| 이영희 | lee@example.com | 사용자 | |
| 박민수 | park@example.com | 편집자 | |
| 최지은 | choi@example.com | 사용자 |
filterValues
{}rowActions 옵션으로 행 삭제/추가를 지원합니다.
deletable: true— 각 행에 삭제 버튼 표시adding: true+ 컬럼의insertable: true— 입력 필드가 자동 렌더링selection과 조합하면 체크박스 선택 후 일괄 삭제 가능classNames의addRow·addInput·addConfirmBtn·addCancelBtn으로 추가 행 스타일 커스터마이징
| Key | 이름 | 이메일 | 역할 | ||
|---|---|---|---|---|---|
| 1 | 홍길동 | hong@example.com | 관리자 | ||
| 2 | 김철수 | kim@example.com | 사용자 | ||
| 3 | 이영희 | lee@example.com | 사용자 | ||
| 4 | 박민수 | park@example.com | 편집자 | ||
| 5 | 최지은 | choi@example.com | 사용자 |
editable: true인 컬럼의 셀을 클릭하면 인라인 편집 모드로 전환됩니다.
validate— 저장 전 유효성 검증, 실패 시errorprops로 메시지 전달onCellChange— 편집 완료(Enter / blur) 시 변경된 값 수신editing.renderCell— 모든 editable 컬럼에 공통 적용되는 편집 UIrenderEditCell— 특정 컬럼만 개별 편집 UI 지정 (renderCell보다 우선)
| Key | 이름 | 이메일 | 역할 |
|---|---|---|---|
| 1 | |||
| 2 | |||
| 3 | |||
| 4 | |||
| 5 |
loading.enabled가 true이면 데이터 대신 shimmer 스켈레톤 행을 렌더링합니다.
rowCount— 스켈레톤 행 개수 (기본값 3)render— 커스텀 스켈레톤 UI (생략 시 기본 shimmer 사용)renderEmpty— 데이터가 없을 때 렌더링할 컴포넌트
| Key | 이름 | 이메일 | 역할 |
|---|---|---|---|
| 1 | 홍길동 | hong@example.com | 관리자 |
| 2 | 김철수 | kim@example.com | 사용자 |
| 3 | 이영희 | lee@example.com | 사용자 |
| 4 | 박민수 | park@example.com | 편집자 |
| 5 | 최지은 | choi@example.com | 사용자 |
대용량 데이터를 위한 가상 스크롤과 무한 로딩을 지원합니다.
scroll.virtual— 가상 스크롤 활성화 (DOM에 보이는 행만 렌더링)scroll.rowHeight— 행 높이(px), 가상 스크롤 사용 시 필수scroll.stickyHeader— 스크롤 시 헤더 고정scroll.onLoadMore— 끝에 도달하면 호출되는 무한 스크롤 콜백classNames.wrap— 고정 높이 +overflow-y: auto또는overflow-y: scroll설정 필수
50 / 300개 로드됨
| ID | 이름 | 이메일 | 부서 | 상태 |
|---|---|---|---|---|
| 1 | 이지은 | user1@example.com | 디자인 | 휴직 |
| 2 | 박예진 | user2@example.com | 마케팅 | 수습 |
| 3 | 최준서 | user3@example.com | 영업 | 재직 |
| 4 | 정도현 | user4@example.com | 인사 | 휴직 |
| 5 | 강현우 | user5@example.com | 재무 | 수습 |
| 6 | 조나연 | user6@example.com | 개발 | 재직 |
| 7 | 윤서연 | user7@example.com | 디자인 | 휴직 |
| 8 | 장수빈 | user8@example.com | 마케팅 | 수습 |
| 9 | 임태양 | user9@example.com | 영업 | 재직 |
| 10 | 김민준 | user10@example.com | 인사 | 휴직 |
| 11 | 이지은 | user11@example.com | 재무 | 수습 |
| 12 | 박예진 | user12@example.com | 개발 | 재직 |
| 13 | 최준서 | user13@example.com | 디자인 | 휴직 |
| 14 | 정도현 | user14@example.com | 마케팅 | 수습 |
| 15 | 강현우 | user15@example.com | 영업 | 재직 |
| 16 | 조나연 | user16@example.com | 인사 | 휴직 |
| 17 | 윤서연 | user17@example.com | 재무 | 수습 |
| 18 | 장수빈 | user18@example.com | 개발 | 재직 |
컬럼 숨김, 순서 변경, 고정(pin), 리사이즈를 지원합니다. 헤더 메뉴에서 컬럼 관리 모달을 열고, 드래그로 순서를 변경할 수 있습니다.
| 이름 | 이메일 | 부서 | 직책 | 입사일 | |
|---|---|---|---|---|---|
| 홍길동 | hong@example.com | 개발 | 팀장 | 2020-03-15 | |
| 김철수 | kim@example.com | 디자인 | 시니어 | 2021-07-01 | |
| 이영희 | lee@example.com | 마케팅 | 매니저 | 2019-11-20 | |
| 박민수 | park@example.com | 개발 | 주니어 | 2023-01-10 | |
| 최지은 | choi@example.com | 영업 | 팀장 | 2018-05-22 |
상태
hiddenKeys: []
order: []
pinned: {}
widths: {}
ExpandDef로 그룹(부모)과 자식 행을 구분하여 계층형 테이블을 구성합니다. 자식 행 선택, 삭제, 아이콘 커스터마이징을 지원하며 childExpandDef로 다단계 중첩도 가능합니다.
| 이름 | 직책 | 이메일 | ||
|---|---|---|---|---|
| chevron_right개발본부 (2팀 · 5명) | ||||
| chevron_right디자인본부 (1팀 · 2명) | ||||
상태
expandedKeys: []
selectedChildKeys: []
| 이름 | 부서 | 직책 (편집 가능) | 상태 |
|---|---|---|---|
| 홍길동 | 개발 | 재직 | |
| 김철수 | 개발 | 재직 | |
| 이영희 | 디자인 | 재직 | |
| 박민준 | 기획 | 휴직 | |
| 최지은 | 개발 | 재직 |
포커스 셀
없음이벤트 로그
아직 이벤트가 없습니다.
tooltip을 켜면 셀 호버 시 값이 툴팁으로 표시됩니다.copyable을 켜면 copyable: true인 열에서 우클릭 컨텍스트 메뉴가 열립니다.onCellCopy로 복사 이벤트를 수신하고, cellContextMenuItems로 메뉴 항목을 추가할 수 있습니다.
상품명·SKU 셀에 호버하면 툴팁이 표시됩니다. 우클릭하면 복사 메뉴가 열립니다.
| 상품명 | SKU | 카테고리 | 가격 |
|---|---|---|---|
MacBook Pro 14인치 M3 Pro 칩 36GB RAM 512GB SSD | MBP-14-M3PRO-36-512 | 노트북 | 2,990,000원 |
iPhone 15 Pro Max 256GB 티타늄 내추럴 | IPH-15PROMAX-256-NT | 스마트폰 | 1,890,000원 |
iPad Air M2 256GB Wi-Fi + Cellular 스타라이트 | IPA-M2-256-CELL-SL | 태블릿 | 1,149,000원 |
AirPods Pro 2세대 USB-C MagSafe 충전 케이스 | APP-2G-USBC-MAGSAFE | 이어폰 | 359,000원 |
Apple Watch Ultra 2 49mm 티타늄 오션 밴드 | AW-ULTRA2-49-OCEAN | 스마트워치 | 1,249,000원 |
복사 로그
아직 복사된 값이 없습니다.
classNames prop으로 모든 시각적 요소에 className 슬롯이 제공됩니다. 아래에서 테마를 선택해 차이를 확인하세요.
| 이름 | 역할 | 부서 | 점수 |
|---|---|---|---|
| 홍길동 | 개발자 | 개발팀 | 92 |
| 김철수 | 디자이너 | 디자인팀 | 88 |
| 이영희 | 기획자 | 기획팀 | 95 |
| 박민준 | 개발자 | 개발팀 | 78 |
| 최지수 | 마케터 | 마케팅팀 | 84 |
적용된 classNames — 기본표준 패딩과 구분선
<Table
columns={columns}
data={data}
rowKey={(row) => String(row.id)}
// ...기타 props
classNames={{
table: "w-full text-sm",
thead: "bg-surface-container-low text-on-surface-variant",
th: "px-4 py-3 text-left font-semibold",
tr: "border-t border-outline-variant/20 hover:bg-surface-container-lowest transition-colors",
td: "px-4 py-3 text-on-surface",
}}
/>| 슬롯 | 적용 요소 | 관련 기능 | 사용 예시 |
|---|---|---|---|
| 기본 레이아웃 | |||
| wrap | 테이블 최외곽 div | 스크롤 컨테이너 (height + overflow-y 설정) | height: 400px; overflow-y: auto |
| table | <table> | 기본 | border-collapse: collapse; width: 100% |
| thead | <thead> | 기본 | position: sticky; top: 0; background: #fff; z-index: 10 |
| th | <th> (헤더 셀) | 기본 | padding: 8px 16px; text-align: left; font-weight: 600 |
| tbody | <tbody> | 기본 | border-top: 1px solid #eee |
| tr | <tr> (데이터 행) | 기본 | border-bottom: 1px solid #eee |
| td | <td> (데이터 셀) | 기본 | padding: 8px 16px; font-size: 14px |
| 정렬 | |||
| sortPriority | 다중 정렬 우선순위 숫자 | 정렬 | color: #3b82f6; font-size: 11px; margin-left: 4px |
| 체크박스 선택 | |||
| checkbox | <input type='checkbox'> | 선택 | accent-color: #3b82f6; cursor: pointer |
| checkboxChecked | 체크된 상태의 체크박스 | 선택 | outline: 2px solid #3b82f6 |
| 필터 | |||
| filterRow | 필터 입력 행 <tr> | 필터 | background: #f5f5f5 |
| filterCell | 필터 행의 <th> | 필터 | padding: 6px |
| filterInput | 텍스트 / 날짜 / 숫자 범위 <input> | 필터 | border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; width: 100% |
| filterSelect | select 필터 <select> | 필터 | border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px |
| 인라인 편집 | |||
| tdEditing | 편집 중인 <td> | 인라인 편집 | outline: 2px solid #3b82f6; background: #eff6ff |
| editError | 편집 에러 메시지 | 인라인 편집 | color: #ef4444; font-size: 12px; margin-top: 4px |
| tdFocused | 키보드 포커스된 <td> | 키보드 내비게이션 | background: rgba(99, 102, 241, 0.1) |
| 행 삭제 / 추가 | |||
| addRow | 새 행 추가 <tr> | 행 추가 | background: #f0fdf4 |
| addInput | 추가 행의 입력 필드 | 행 추가 | border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; width: 100% |
| addConfirmBtn | 추가 확인 버튼 | 행 추가 | color: #16a34a; cursor: pointer |
| addCancelBtn | 추가 취소 버튼 | 행 추가 | color: #9ca3af; cursor: pointer |
| 헤더 메뉴 | |||
| headerMenuBtn | ⋯ 버튼 | 헤더 메뉴 | opacity: 0; transition: opacity 0.2s |
| headerMenuDropdown | 드롭다운 컨테이너 | 헤더 메뉴 | box-shadow: 0 4px 12px rgba(0,0,0,.1); border-radius: 8px; background: #fff |
| headerMenuItem | 드롭다운 각 항목 <button> | 헤더 메뉴 | padding: 8px 16px; font-size: 14px; text-align: left; width: 100% |
| 컬럼 관리 | |||
| columnManagerBackdrop | 모달 배경 오버레이 | 컬럼 관리 | background: rgba(0,0,0,.4); backdrop-filter: blur(4px) |
| columnManager | 모달 컨테이너 | 컬럼 관리 | border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,.2); background: #fff; width: 320px |
| columnManagerHeader | 모달 헤더 | 컬럼 관리 | display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #eee; padding: 16px |
| columnManagerTitle | 모달 제목 | 컬럼 관리 | font-weight: 700; font-size: 15px |
| columnManagerSelectAllBtn | 전체 선택 버튼 | 컬럼 관리 | color: #3b82f6; font-size: 12px |
| columnManagerDeselectAllBtn | 전체 해제 버튼 | 컬럼 관리 | color: #9ca3af; font-size: 12px |
| columnManagerCloseBtn | 닫기 버튼 | 컬럼 관리 | color: #9ca3af; cursor: pointer |
| columnManagerBody | 모달 본문 (토글 목록 영역) | 컬럼 관리 | padding: 12px; max-height: 240px; overflow-y: auto |
| columnToggle | 컬럼 토글 항목 | 컬럼 관리 | display: flex; align-items: center; gap: 8px; padding: 6px 0 |
| columnToggleActive | 표시 중인 컬럼 토글 | 컬럼 관리 | color: #3b82f6; font-weight: 500 |
| columnToggleCheckbox | 토글 체크박스 | 컬럼 관리 | accent-color: #3b82f6 |
| 컬럼 리사이즈 / 드래그 / 핀 | |||
| resizeHandle | 리사이즈 핸들 div | 컬럼 리사이즈 | width: 4px; height: 100%; cursor: col-resize; background: #e5e7eb |
| thDragging | 드래그 중인 <th> | 컬럼 드래그 순서 변경 | opacity: 0.4 |
| thDragOver | 드래그 오버된 <th> | 컬럼 드래그 순서 변경 | border-left: 2px solid #6366f1 |
| thPinnedLeft | 좌측 고정 <th> | 컬럼 핀 | position: sticky; left: 0; background: #fff; box-shadow: 2px 0 4px rgba(0,0,0,.08) |
| thPinnedRight | 우측 고정 <th> | 컬럼 핀 | position: sticky; right: 0; background: #fff; box-shadow: -2px 0 4px rgba(0,0,0,.08) |
| tdPinnedLeft | 좌측 고정 <td> | 컬럼 핀 | position: sticky; left: 0; background: #fff |
| tdPinnedRight | 우측 고정 <td> | 컬럼 핀 | position: sticky; right: 0; background: #fff |
| 컨텍스트 메뉴 / 툴팁 / 복사 | |||
| contextMenu | 헤더 우클릭 컨텍스트 메뉴 | 컬럼 핀 컨텍스트 메뉴 | border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.1); background: #fff; padding: 4px 0 |
| contextMenuItem | 컨텍스트 메뉴 항목 | 컬럼 핀 컨텍스트 메뉴 | padding: 8px 16px; font-size: 14px; cursor: pointer; width: 100% |
| tooltip | 셀 호버 툴팁 | 툴팁 | background: #1f2937; color: #fff; font-size: 12px; border-radius: 4px; padding: 4px 8px |
| cellCopyMenu | 셀 우클릭 복사 메뉴 | 복사 | border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.1); background: #fff |
| cellCopyMenuItem | 복사 메뉴 항목 | 복사 | padding: 8px 16px; font-size: 14px; width: 100% |
| 확장 행 | |||
| groupRow | 그룹(부모) 행 <tr> | 확장 행 | background: #f5f5f5; font-weight: 600 |
| groupCell | 그룹 행 <td> | 확장 행 | cursor: pointer; user-select: none |
| expandIcon | 펼치기 / 접기 아이콘 | 확장 행 | transition: transform 0.2s |
| childRow | 자식 행 <tr> | 확장 행 | background: rgba(255,255,255,.5) |
| childTd | 자식 행 <td> | 확장 행 | padding: 8px 0 |
| childIndent | 자식 행 들여쓰기 div | 확장 행 | width: 20px; flex-shrink: 0; border-left: 1px solid #e5e7eb |
| 로딩 / 빈 상태 / 가상 스크롤 | |||
| skeletonRow | 스켈레톤 행 <tr> | 로딩 | animation: pulse 1.5s ease-in-out infinite |
| skeletonCell | 스켈레톤 <td> | 로딩 | padding: 12px 16px |
| skeletonBar | 스켈레톤 바 (shimmer 애니메이션) | 로딩 | height: 16px; background: #e5e7eb; border-radius: 9999px |
| emptyRow | 빈 상태 행 <tr> | 빈 상태 | text-align: center |
| emptyCell | 빈 상태 <td> | 빈 상태 | padding: 80px 0; color: #9ca3af |
| loadMoreRow | 더 불러오기 행 <tr> | 가상 스크롤 | text-align: center |
| loadMoreCell | 더 불러오기 <td> | 가상 스크롤 | padding: 16px |
| sentinelRow | 무한 스크롤 감지 행 | 가상 스크롤 | height: 1px; visibility: hidden |
| virtualPadding | 가상 스크롤 상·하단 패딩 행 | 가상 스크롤 | background: transparent |
ColumnDef의 전체 옵션 목록입니다.
const columns: ColumnDef<User>[] = [
{
key: 'name',
label: '이름',
render: (row) => row.name,
width: '200px',
minWidth: 80,
sortable: true,
filterType: 'text',
filterOptions: [
{ label: '관리자', value: 'admin' },
],
filterPlaceholder: '이름 검색...',
editable: true,
insertable: true,
align: 'left',
copyable: true,
validate: (value) => value.trim() ? null : '필수 항목입니다.',
renderEditCell: (props) => <CustomInput {...props} />,
onValidationError: (error) => console.warn(error),
},
]| 옵션 | 타입 | 설명 |
|---|---|---|
| key | string | 컬럼 고유 키 |
| label | string | 헤더 표시 텍스트 |
| render | (row) => ReactNode | 셀 렌더 함수 |
| width | string | CSS 너비 (px, %) |
| minWidth | number | 최소 너비 (px) |
| sortable | boolean | 정렬 가능 여부 |
| filterType | string | text | select | dateRange | numberRange |
| filterOptions | { label: string; value: string }[] | select 필터의 선택지 목록 |
| filterPlaceholder | string | text 필터 입력창 placeholder |
| editable | boolean | 인라인 편집 가능 |
| insertable | boolean | 행 추가 시 입력 가능 |
| align | string | left | center | right |
| copyable | boolean | 셀 복사 가능 |
| validate | (value) => string | null | 편집 검증 함수 |
| renderEditCell | (props) => ReactNode | 커스텀 편집 셀 렌더러 (editing.renderCell보다 우선) |
| renderInsertCell | (props) => ReactNode | 행 추가 시 커스텀 입력 셀 렌더러 |
| onValidationError | (error: string) => void | 검증 실패 시 콜백 — 설정 시 renderCell의 error props가 null이 됨 |