[ React ] CommonTable 컴포넌트 기본 구현 (정렬, 체크박스, 스크롤 등)

2024. 7. 16. 22:53· React
목차
  1. 요구사항
  2. 구현
  3. 1. 구조
  4. 2. 데이터 매핑
  5. 3. 정렬
  6. 4. 행 선택 (체크박스)
  7. 5. 셀 색상 (글자색, 배경색)
  8. 6. 셀 더블클릭 이벤트
  9. 7. 스크롤
728x90

요구사항

  • Only View
  • 정렬
  • 행 선택 (체크박스)
  • 셀 색상 (글자색, 배경색) → 메서드로 구현
  • 셀 더블클릭 이벤트
  • 마우스 hover 시 행 하이라이팅
  • 가로/세로 스크롤

 


구현

1. 구조

<table>
  <thead>
    <th />
    ...
    <th />
  </thead>
  <tbody>
    <tr>
      <td />
    </tr>
    ...
    <tr>
      <td />
    </tr>
  </tbody>
</table>

 

 

 

2. 데이터 매핑

<th id=””> 와 <td headers=””> 사용

즉 헤더의 id와 같은 headers 값을 가진 데이터를 매핑해준다.

 

 

 

3. 정렬

컬럼 헤더 클릭 → 클릭한 컬럼 정보 props로 넘겨줌 → 조건 맞춰 rowData 정렬

  • 숫자는 크기 순으로
  • 문자는 ㄱㄴㄷ, abc 순으로
  • 클릭 시 동작 순서 : 오름차순 → 내림차순 → 원래대로
 

[ Javascript ] 정렬 함수 (sort, toSorted, localeCompare)

sort Array.prototype.sort() - JavaScript | MDNsort() 메서드는 배열의 요소를 적절한 위치에 정렬한 후 그 배열을 반환합니다. 정렬은 stable sort가 아닐 수 있습니다. 기본 정렬 순서는 문자열의 유니코드 코

zodev.tistory.com

 

결론적으로 sort + localeCompare 사용

 

 

정렬 함수

import type { RowData } from "../types/grid";

// 오름차순
export const AscendingSort = (rowData: RowData[], col: string) => {
  return [...rowData].sort((x, y) => {
    if (x[col] === y[col]) return 0;
    if (x[col] === undefined || x[col] === null) return -1;
    if (y[col] === undefined || y[col] === null) return 1;
    if (typeof x[col] === "string" && typeof y[col] === "string") {
      return x[col].localeCompare(y[col], undefined, { numeric: true });
    } else return x[col] - y[col];
  });
};

// 내림차순
export const DescendingSort = (rowData: RowData[], col: string) => {
  return [...rowData].sort((x, y) => {
    if (x[col] === y[col]) return 0;
    if (x[col] === undefined || x[col] === null) return 1;
    if (y[col] === undefined || y[col] === null) return -1;
    if (typeof x[col] === "string" && typeof y[col] === "string") {
      return y[col].localeCompare(x[col], undefined, { numeric: true });
    } else return y[col] - x[col];
  });
};

비교하는 두 값이 동일할 경우 변동없이 원래의 순서대로 위치하도록 했다.

값이 없거나(undefined) null일 때는 가장 작은 값으로 취급해 오름차순일 경우 가장 상위로, 내림차순일 경우 가장 하위로 보내줬다.

문자열 타입의 경우 localeCompare 함수를 이용했고, 그 외의 경우 단순히 사칙연산을 이용해 코드를 크기를 비교했다. (=순서를 정했다.)

 

 

정렬 동작 순서

오름차순 → 내림차순 → 원래대로의 순서를 적용하기 위해 isSort라는 변수를 추가했다. (이름과 달리 숫자 타입이다…)

const [isSort, setIsSort] = useState(0); // 0:정렬X, 1:오름차순, 2:내림차순

해당 숫자에 따라 다음에 어떤 동작을 시행할지 switch 문을 사용해서 구분했다.

 

 

다른 컬럼 클릭 시

그러나 다른 컬럼을 클릭 시 새롭게 정렬을 시작해야 하므로 어떤 컬럼을 정렬하고 있는지 저장하는 변수를 추가했다.

const [sortCol, setSortCol] = useState("");

 

// 헤더 셀 클릭 시 정렬 추가
const SortRowData = (col: string) => {
  if (col !== sortCol) {
    setViewData(AscendingSort(rowData, col));
    setIsSort(1);
    setSortCol(col);
  } else {
    switch (isSort) {
      case 0: // 정렬 X 상태
        setViewData(AscendingSort(rowData, col));
        setIsSort(1);
        return;
      case 1: // 오름차순 상태
        setViewData(DescendingSort(rowData, col));
        setIsSort(2);
        return;
      case 2: // 내림차순 상태
        setViewData(rowData);
        setIsSort(0);
        return;
    }
  }
};

 

정렬

 

 

 

4. 행 선택 (체크박스)

checkbox 속성 활성화 시 행 선택 가능한 checkbox를 그리드 가장 왼쪽 열에 생성

  • 전체 선택
  • 개별 선택

 

어떻게 구현할까?

❌ 체크박스에 체킹된 행들을 배열에 보관? → 그럼 스타일링 줄 때 in 연산자를 사용해야 해서 성능이 악화될 위험이 있다.

✅ rowData를 변경? → 정렬할 때도 체크된 상태 지니기 위해 rowData의 복사본인 viewData 사용 → 복사본 배열 내 객체에 새로운 key:value 쌍을 추가 (isChecked: boolean)

useEffect로 처음 마운트됐을 때 viewData와 viewCol에 props로 받은 colDefs + isChecked, rowData + isChecked 배열 할당

  useEffect(() => {
    if (!checkbox) return;
    const newCol = {
      name: "isChecked",
      field: "isChecked",
      sortable: false,
    };
    setViewData(
      rowData.map((data) => ({
        ...data,
        isChecked: "false",
      }))
    );
    setViewCol([newCol, ...colDefs]);
  }, []);

 

 

체크박스 클릭 시 동작

배열 내 객체가 id 값을 가지고 있지 않으므로 index로 제어

react에서 배열의 변경을 감지하게 하려면 새로운 배열을 setState 함수에 할당하여 state의 주소값을 바꿔줘야 한다.

// 모든 체크박스 제어
const checkAllCell = (e: React.ChangeEvent<HTMLInputElement>) => {
  const newData = viewData.map((data) => ({
    ...data,
    isChecked: e.target.checked,
  }));
  setViewData(newData);
  setAllChecked(e.target.checked);
};

// 개별 체크박스 제어
const checkEachCell = (
  e: React.ChangeEvent<HTMLInputElement>,
  idx: number
) => {
  const newData = [...viewData];
  newData[idx] = { ...newData[idx], isChecked: e.target.checked };
  setViewData(newData);
};
  • 배열 복사본 생성
  • 모든 객체 또는 해당 idx의 객체의 isChecked 값 변경
  • setState 함수에 새로운 배열 복사본 할당

스타일은 <td> 태그에 다음 속성 추가

$checked={item["isChecked"] || false}

 

 

체크박스와 정렬 충돌 문제

정렬 시 props로 받은 원본 데이터인 rowData를 이용했는데, 이 경우 isChecked 변수가 없다.

단순히 정렬 시 사용하는 데이터를 viewData로 바꾼다하더라도 여러 문제가 발생한다. (원래대로 돌아오려면 기존 순서를 지닌 배열을 따로 보관한다. → 정렬 시에 새로운 행이 선택되더라도 index 값을 사용하기 때문에 순서 배열은 이 값을 알 수 없다. 등등)

그래서 isChecked를 추가할 때 order라는 순서를 가진 변수를 함께 추가해줬다.

useEffect(() => {
  if (!checkbox) {
    setViewData(
      rowData.map((data, idx) => ({
        ...data,
        order: idx,
      }))
    );
    setViewCol(colDefs);
    return;
  }

  const newCol = {
    name: "isChecked",
    field: "isChecked",
    sortable: false,
  };
  setViewData(
    rowData.map((data, idx) => ({
      ...data,
      isChecked: false,
      order: idx,
    }))
  );
  setViewCol([newCol, ...colDefs]);
}, []);

그리고 정렬 시 사용하는 데이터를 rowData에서 viewData로 바꿔줬다.

// 헤더 셀 클릭 시 정렬 추가
const SortRowData = (col: string) => {
  if (col !== sortCol) {
    setViewData(AscendingSort(viewData, col));
    setIsSort(1);
    setSortCol(col);
  } else {
    switch (isSort) {
      case 0: // 정렬 X 상태
        setViewData(AscendingSort(viewData, col));
        setIsSort(1);
        return;
      case 1: // 오름차순 상태
        setViewData(DescendingSort(viewData, col));
        setIsSort(2);
        return;
      case 2: // 내림차순 상태
        setViewData(AscendingSort(viewData, "order"));
        setIsSort(0);
        return;
    }
  }
};

 

정렬 + 행 선택

 

 

😎 체크박스 전체 선택/해제 시에는 헤더 체크박스에 변화가 있는 게 자연스러움

  1. isChecked: true의 개수를 센다. ✅
  2. 배열을 돌면서 확인한다. → 비효율적

🍀🍀🍀 추가 : 체크박스 하나라도 해제 시에는 헤더 체크박스도 false로 변환 🍀🍀🍀

 

🍀🍀🍀 추가 : 수동으로 모든 체크박스 선택했을 시 헤더 체크박스도 true로 변환 🍀🍀🍀

→ 체크된 행 수 저장하는 변수 추가

const [checkCnt, setCheckCnt] = useState(0);

 

체크박스 개별/전체 선택 함수 실행 시 조건에 따라 checkCnt와 allChecked 변수를 변경해줬다.

 

 

 

5. 셀 색상 (글자색, 배경색)

상위에서 셀 스타일을 커스텀하는 함수를 정의한 후 공통 컴포넌트에서 props로 전달 받는 방식을 사용했다. → cellStyleHandler

export interface GridProps {
  colDefs: ColDefs[];
  rowData: RowData[];
  checkbox?: boolean;
  cellStyleHandler: (columnName: string, rowContent: unknown) => object;
}

각 셀을 나타내는 td 태그에 style={cellStyleHandler(col.field, item[col.field])} 부여

// 상위 함수 예시

const cellStyleHandler = (columnName: string, rowContent: any) => {
  if (columnName === "a") return { backgroundColor: "#ffff00" };
  if (columnName === "c" && rowContent < "2023-01-01")
    return {
      backgroundColor: "#ffa500",
    };
};

 

셀 색상

 

 

 

6. 셀 더블클릭 이벤트

cellStyleHandler와 동일하게 상위 컴포넌트에서 정의해 둔 함수를 CommonTable에 props로 전달하는 방법을 사용했다.

export interface GridProps {
  colDefs: ColDefs[];
  rowData: RowData[];
  checkbox?: boolean;
  cellStyleHandler: (columnName: string, rowContent: unknown) => object;
  cellDoubleClickHandler: (e: React.MouseEvent) => void;
}
onDoubleClick={cellDoubleClickHandler}

 

더블클릭 event의 타입은 MouseEvent이다.

 

// 상위 컴포넌트 예시 코드
const cellDoubleClickHandler = (e: React.MouseEvent) => {
  const target = e.target as HTMLTableCellElement;
  console.log(target.textContent);
  console.log(target.headers);
};

e.target을 HTMLTableCellElement로 타입 단언을 해줘야 TS 컴파일러가 내뱉는 많은 에러를 벗어날 수 있다.

단순히 HTMLElement를 단언하면 td 태그만 지닌 headers 등의 속성은 찾지 못하기 때문에 HTMLTableCellElement를 써줘야 한다.

 

이때 셀이 가진 데이터 값은 textContent, 컬럼명은 headers로 가져올 수 있다.

 

더블클릭

 

 

 

7. 스크롤

display: block;

display 속성이 table이면 overflow가 적용되지 않는다. 그렇기 때문에 display 속성을 block으로 바꿔줘야 한다.

처음에는 thead, tbody 각각을 바꿔줬는데, 그 경우 table의 가로 스크롤 생성을 위해서는 상위 요소에 overflow: auto 속성이 적용되어야 했다.

그 컨트롤 부분을 table 내부로 옮기기 위해 table 태그에 display: block 속성을 부여했다.

export const Table = styled.table`
  width: 100%;
  height: 100%;
  overflow: auto;
  display: block; // 스크롤 적용 위해 추가
	...
`

 

 

세로 스크롤

헤더는 고정한 채 데이터 행들만 스크롤 생성

thead에 sticky 속성을 주어 스크롤이 생기더라도 상단에 고정되도록 했다.

thead {
  height: 3rem;
  position: sticky;
  top: 0;
}

 

 

스크롤 CSS

기본 스크롤은 너무 못생겼으니까 스크롤 디자인을 변경해줬다.

  &::-webkit-scrollbar {
    width: 0.7rem;
    height: 0.7rem;
  }

  &::-webkit-scrollbar-thumb {
    background-clip: padding-box;
    border: 1px solid transparent;
    border-radius: 3rem;
    background-color: #dddddd;
  }

  &::-webkit-scrollbar-track {
    background-color: ${({ theme }) => theme.colors.grayBg};
  }

 

스크롤

 

 

 

다음번에는 이 테이블에 편집 기능을 넣어서 올 예정이다. 그정도면 차라리 라이브러리를 쓰는게 낫지 않나.. 라는 생각이 들 수도 있지만 (나도 들지만) 필요한 기능만 딱 넣어서 사용하기엔 나쁘지 않은 것 같기도 해서 일단 츄라이츄라이~

728x90

'React' 카테고리의 다른 글

[ React ] React에서 slot 사용하기  (0) 2024.11.14
[ React ] CommonTable 편집모드 (데이터 변경, 행 추가, 삭제)  (0) 2024.08.29
[ React ] DatePicker, SelectBox의 팝업을 createPortal로 변경  (0) 2024.07.12
[ React ] SelectBox 컴포넌트 키보드 이벤트 추가 | onKeyDown  (0) 2024.07.11
[ React ] input 태그의 placeholder로 icon 사용하기  (0) 2024.07.10
  1. 요구사항
  2. 구현
  3. 1. 구조
  4. 2. 데이터 매핑
  5. 3. 정렬
  6. 4. 행 선택 (체크박스)
  7. 5. 셀 색상 (글자색, 배경색)
  8. 6. 셀 더블클릭 이벤트
  9. 7. 스크롤
'React' 카테고리의 다른 글
  • [ React ] React에서 slot 사용하기
  • [ React ] CommonTable 편집모드 (데이터 변경, 행 추가, 삭제)
  • [ React ] DatePicker, SelectBox의 팝업을 createPortal로 변경
  • [ React ] SelectBox 컴포넌트 키보드 이벤트 추가 | onKeyDown
ZoD
ZoD
바쁘게 굴러가는 ZoD의 하루~
ZoD
como siempre
ZoD
  • 분류 전체보기 (173)
    • Daily (0)
    • Javascript (16)
      • 모던 자바스크립트 (46)
    • Typescript (8)
    • FrontEnd (7)
    • React (21)
      • tanstack query (1)
      • 라이브러리 (3)
    • Vue (33)
      • Nuxt (11)
      • Quasar (6)
    • CSS (12)
    • Storybook (8)
    • Figma (3)
    • CS 지식 (5)
    • Network (5)
    • 알고리즘 (2)
    • 사이드 프로젝트 (4)
      • ddangkong (4)
      • boncierge (0)
    • 개발, 근데 이제 헛소리를 곁들인 (1)
    • ETC (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

최근 글

hELLO · Designed By 정상우.v4.2.2
ZoD
[ React ] CommonTable 컴포넌트 기본 구현 (정렬, 체크박스, 스크롤 등)
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.