Tanstack-Table

하니·2024년 8월 13일

React 라이브러리

목록 보기
1/5
post-thumbnail

프로젝트에 투입되면서 프론트엔드 팀원들과 Radix-UI 라이브러리를 사용해서 요구된 ui를 그리기로 결정했다. 하지만 radix는 단순히 ui만 담당하고 있었기 때문에 관리자 페이지에서 다양한 표 기능을 사용하기에는 부족했다. 그래서 기능까지 제공하고 있는 Tanstack-Table을 사용하기로 했다.

Tanstack Table : 테이블을 만들어주는 라이브러리

Radix-UI : 구성 요소와 UI 패턴을 제공하는 라이브러리

Tanstack-Table 라이브러리를 사용하면서 헷갈렸던 부분들을 적고자 한다.

표에서 Checkbox 다루기

👧🏻 hani's 컴포넌트 구성 방법
상태는 부모가 관리하고, 자식은 부모로부터 전달받은 함수를 통해 상태를 업데이트하도록 구성했다.
즉, 부모 컴포넌트TaxIndex에서 모든 상태와 함수를 관리하고, 자식 컴포넌트Table에서는 따로 상태를 선언하지 않고 부모로부터 모두 전달받도록 했다.

Tanstack-Table에서 제공하는 API를 통해 Checkbox 다루기

Tanstack-Table 공식문서 : Row Selection 가이드

부모 컴포넌트
RowSelectionState : [타입] 자신의 행 선택 상태를 관리한다.

// TaxIndex(부모 컴포넌트)

// 1. 테이블의 체크박스 상태를 담아줄 상태를 선언한다.
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})

...
 
// 2. 테이블의 체크박스 상태를 처리해줄 함수를 선언한다.
const handleCheckChange = (selection: RowSelectionState) => {
  setRowSelection(selection)
}

...

// tsx
         {/* 테이블 */}
          <Table
            pageSize={size}
            date={{ startDate, endDate }}
            onPageChanage={onPageChange}
            forcePage={page}
            initialPage={page}
            pageCount={0}
            rowSelection={rowSelection} // 3. 상태 변수와 함수를 props로 넘겨준다.
            onRowSelectionChange={handleCheckChange} // 3.
          />
        </div>
      </div>
    </ParentContainer>
  )
}

자식 컴포넌트 (Table)

columnHelper를 통해 표의 columns를 정의하고 구성한다고 생각하면 된다.

✔️ Column Def Type
Accessor Columns : 기본 데이터 모델이 있어서 정렬, 필터링, 그룹화 등이 가능하다.

Display Columns : 데이터 모델이 없으므로 정렬, 필터링 등이 불가능하지만 표의 임의의 콘텐츠를 표시하는 데 사용할 수 있다. ex) 버튼, 체크박스..

✔️ Row Selection UI 렌더링 API
[체크여부]

  • table.getIsAllRowsSelected() : 모든 체크박스가 체크되어 있는지 여부를 반환한다.
  • row.getIsSelected() : 해당 행의 체크박스가 체크되어 있는지 여부를 반환한다.

[함수]

  • table.toggleAllRowsSelected() : 테이블의 모든 행의 선택 상태를 토글(변경)한다.
    (value 파라미터가 true이면 모든 행을 선택하고, false이면 모든 행의 선택을 해제한다.)
    (-> table.getToggleAllRowsSelectedHandler() API보다 세부적인 제어가 필요한 경우 사용한다.)
  • row.getToggleSelectedHandler() : 특정 행의 선택 상태를 토글(변경)하는 핸들러 함수를 반환한다. 즉, 사용자가 해당 행의 체크박스를 클릭했을 때 호출되어 해당 행의 선택 상태를 변경하는 역할을 한다.

✅ 코드의 추가적인 설명
여기에서 사용된 Checkbox컴포넌트는 radix-ui를 사용해서 만든 공통 컴포넌트이다. radix에서 제공하는 CheckboxProps를 사용하고 있기 때문에 props로 checked, onCheckedChange를 넘겨줘야 한다. 이때 onCheckedChange에는 해당 체크박스가 바뀔 때(체크 유무) 체크의 상태를 바꿔주는 함수를 넣어주면 된다.

❗️이때 table.toggleAllRowsSelected() 대신에 table.getToggleAllRowsSelectedHandler()를 써주면, 확실하지는 않지만 radix-ui의 onCheckedChange 이벤트 핸들러와 호환되지 않아 에러가 발생한다.
따라서 직접적으로 boolean 값을 처리하는 방법을 선택해야 한다.

이중 부정!! :어떤 값이든지 명시적으로 boolean으로 변환하기 위해서 사용한다. 특정 함수가 boolean 값을 기대할 때, 이중 부정을 사용하여 값을 boolean으로 강제 변환할 수 있다.

// Table(자식 컴포넌트)

// * 테이블 col 정보
const columnHelper = createColumnHelper<TableData>()

const columns = [
  // 체크박스
  columnHelper.display({
    id: 'select',
    // header에 들어가는 {전체 선택 체크버튼}
    header: ({ table }) => (
      <Checkbox
        checked={table.getIsAllRowsSelected()}
        onCheckedChange={value => table.toggleAllRowsSelected(!!value)}
      />
    ),
    // columns에 들어가는 {부분 선택 체크버튼}
    cell: ({ row }) => <Checkbox checked={row.getIsSelected()} onCheckedChange={row.getToggleSelectedHandler()} />,
  }),
    
  columnHelper.accessor('publicationDate', {
    id: 'publicationDate',
    header: '발행일',
    cell: info => info.getValue().toLocaleDateString(),
  }),

이제 부모 컴포넌트로부터 전달 받은 rowSelection상태와 onRowSelectionChange함수를 사용하여 테이블의 상태를 관리하고 변경사항을 부모에게 전달해야 한다.

// table(자식 컴포넌트)

// 테이블 설정 부분
const table = useReactTable({
  data,
  columns,

  ...

  state: {// 테이블의 상태 정의
    rowSelection, // 1. 행 선택 상태를 다시 테이블 인스턴스로 전달한다. (rowSelection의 타입은 RowSelectionState이기 때문에 가능하다?)
  },
  onRowSelectionChange: (selection: RowSelectionState) => {
    onRowSelectionChange(selection) // 2. 부모 컴포넌트의 핸들러 호출
  },

  ...

})

🌟 흐름 정리
1. 부모 컴포넌트는 자식 컴포넌트에 체크박스의 상태rowSelection와 상태를 변경해주는 함수handleCheckChange를 props로 전달한다.
2. 유저가 체크박스를 클릭하여 체크를 하는 경우 발생
3. 자식 컴포넌트는 radix ui의 Checkbox 컴포넌트를 사용하여 체크박스를 렌더링한다.
4. 자식 컴포넌트에서 체크박스의 상태가 변경되어 props로 전달받은 함수handleCheckChange가 호출된다. 그래서 rowSelection 상태가 업데이트된다.
5. 부모 컴포넌트의 rowSelection상태가 업데이트 되면, 변경된 값이 다시 자식 컴포넌트에 props로 전달되어 ui가 업데이트 된다.
(➡️ 자식 컴포넌트는 useReactTable 훅을 통해 테이블을 초기화하는데 이때, state 속성에서 rowSelection을 설정하여 테이블의 상태를 초기화한다.)

React에서 사용하던 방식대로 Checkbox 다루기

해당 코드는 {전체 선택 체크박스}는 다루고 있지 않다.

부모 컴포넌트

// ProductList (부모 컴포넌트)

const [selectedProducts, setSelectedProducts] = useState<string[]>([]) // 1. 체크박스 상태 선언

// 2. 체크박스 상태 변경 함수
const handleCheckboxChange = (productId: string, isChecked: boolean) => {
    setSelectedProducts(prevSelected =>
      isChecked ? [...prevSelected, productId] : prevSelected.filter(id => id !== productId),
    )
  }

...

// tsx
      <Table
        selectedProducts={selectedProducts} // 3. 상태 변수 넘겨주기
        handleCheckboxChange={handleCheckboxChange} // 3. 함수 넘겨주기
        initialPage={page}
        productList={data?.content || []}
        isLoading={isPending}
        pageCount={data?.totalElements ?? 0}
        onPageChange={onPageChange} 
        forcePage={page}
      />
    </ParentContainer>
  )
}

자식 컴포넌트
현재 행의 ID가 체크 상태 변수selectedProducts에 포함되어 있는지 확인하여 체크박스의 선택 상태를 결정한다.

includes() 함수 : 배열에 특정 값이 포함되어 있는지 여부를 확인한다.

selectedProducts 배열에 String(row.original.id) 값이 포함되어 있는지를 Boolean 값으로 반환한다.
반환 값이 true면, selectedProducts 배열에 현재 행의 ID가 포함되어 있으므로 체크박스는 체크된 상태로 표시된다.

const Table = ({
  handleCheckboxChange,
  initialPage,
  forcePage,
  selectedProducts,
  productList,
  isLoading,
  pageCount,
  onPageChange,
}: TableProps) => {
  const columnHelper = createColumnHelper<Products>()
  
  const columns = [
    // 체크박스
    columnHelper.display({
      id: 'select',
      cell: ({ row }) => {
        return (
          <Checkbox
            checked={selectedProducts.includes(String(row.original.id))} 
            onCheckedChange={(isChecked: boolean) => handleCheckboxChange(String(row.original.id), isChecked)}
          />
        )
      },
    }),
    // 버튼
    columnHelper.display({
      id: 'button',
      header: '수정',
      cell: () => <Button>수정하기</Button>,
    }),

두 가지 방법은 비슷한 듯 다르지만 둘 다 알고 있으면 상황에 맞게 사용할 수 있으므로 나누어 적어보았다.

다음과 같이 테이블이 잘 나오는 것을 확인할 수 있다.

profile
Hi, I am HANI Developer(╹◡╹). .....1hani me?

0개의 댓글