기술 블로그

체크박스 아이템 최적화 본문

프론트엔드

체크박스 아이템 최적화

jaegwan 2026. 3. 13. 12:17
반응형

[react] 대량의 체크박스(5,000건) 렌더링 최적화하기

프로젝트를 진행하다 보면 대량의 데이터를 다뤄야 하는 상황을 마주하곤 합니다. 최근 정산 데이터 수정 모달에서 약 5,000건의 데이터를 테이블로 렌더링하고, 각 행을 체크박스로 선택하는 기능을 구현했습니다.

하지만 구현 후 테스트 과정에서 체크박스를 클릭할 때마다 1~2초 수준의 심각한 지연이 발생했습니다. 이번 포스트에서는 배열 기반 상태 관리의 함정과 이를 SetReact.memo로 해결한 과정을 정리합니다.

1. 문제 상황과 원인 분석

처음에는 일반적인 방식인 배열(Array)을 사용하여 선택된 아이템의 상태를 관리했습니다.

const [selectedItems, setSelectedItems] = useState<DataListEditItem[]>([]);

// 개별 선택 핸들러
const handleSelectRow = (item: DataListEditItem) => {
  setSelectedItems(prev =>
    prev.some(selected => selected.seq === item.seq)
      ? prev.filter(selected => selected.seq !== item.seq)
      : [...prev, item],
  );
};

// 컬럼 정의 내 체크 여부 판단
cell: ({ row }) => (
  <input
    type="checkbox"
    checked={selectedItems.some(item => item.seq === row.original.seq)}
  />
),

언뜻 보면 큰 문제가 없어 보이지만, 데이터가 5,000건 이상으로 늘어나면 시간 복잡도 측면에서 병목이 발생합니다.

시간복잡도로 보는 병목

체크박스 하나를 클릭할 때마다 다음과 같은 연산이 일어납니다.

  1. selectedItems 상태가 변경되어 전체 컴포넌트가 리렌더링됩니다.
  2. 5,000개 행이 모두 다시 그려집니다.
  3. 각 행은 selectedItems.some()을 실행하여 체크 여부를 판단합니다.

Array.some()은 최악의 경우 배열 전체를 순회하는 O(s) 연산입니다(s = 선택된 아이템 수). 이를 5,000개 행마다 반복하므로 전체 비용은 O(n × s)가 됩니다. 만약 전체 선택 후 1개를 해제한다면 약 2,500만 번(5,000 × 5,000)의 비교 연산이 수행되는 셈입니다.


2. 해결 단계 1: Set 기반으로 전환

첫 번째 개선은 조회 성능이 뛰어난 Set 자료구조를 사용하는 것입니다. Set.has()O(1)의 시간 복잡도를 가지므로, 5,000행 전체를 확인해도 O(n)으로 연산량이 급감합니다.

const [selectedSeqs, setSelectedSeqs] = useState<Set<number>>(new Set());

const handleSelectRow = (item: DataListEditItem) => {
  setSelectedSeqs(prev => {
    const next = new Set(prev);
    if (next.has(item.seq)) next.delete(item.seq);
    else next.add(item.seq);
    return next;
  });
};

// 조회 시 O(1)
cell: ({ row }) => (
  <input
    type="checkbox"
    checked={selectedSeqs.has(row.original.seq)}
  />
),
연산 배열 (Before) Set (After)
개별 선택/해제 O(n × s) ≈ O(n²) O(n)
체크 여부 비교 (1행) O(s) O(1)

3. 해결 단계 2: 행 단위 React.memo 적용

Set을 통해 연산 비용은 줄였지만, 여전히 상태가 변경될 때마다 5,000개의 행이 모두 리렌더링되는 근본적인 문제가 남아 있습니다. 이를 해결하기 위해 각 행을 memo로 감싸고 커스텀 비교 함수를 적용했습니다.

// SimpleTable 내부 Row 컴포넌트화
const MemoizedRow = memo(
  <T,>({ row, isSelected }: MemoizedRowProps<T>) => (
    <tr>
      {row.getVisibleCells().map(cell => (
        <td key={cell.id}>
          {flexRender(cell.column.columnDef.cell, cell.getContext())}
        </td>
      ))}
    </tr>
  ),
  (prev, next) =>
    prev.row.original === next.row.original && 
    prev.isSelected === next.isSelected // 데이터와 선택 상태가 같으면 리렌더링 방지
);

이 방식을 통해 체크박스 하나를 클릭했을 때, 실제 변경이 일어난 단 1개의 행만 다시 그려지도록 최적화했습니다.


4. 최종 비교 및 정리

각 단계별 최적화 결과는 다음과 같습니다.

구분 배열 기반 Set 전환 Set + Memo
checked 비교 O(s) O(1) O(1)
개별 클릭 시 리렌더링 5,000개 행 5,000개 행 1개 행
5,000건 체감 속도 1~2초 지연 다소 개선 즉시 반응

데이터셋이 작을 때는 배열 기반으로도 충분하지만, 수천 건 이상의 인터랙티브한 테이블을 구현해야 한다면 Set을 통한 조회 최적화memo를 통한 렌더링 최적화 조합을 필수적으로 고려해야 합니다.

특히 Set으로의 전환은 코드 수정량이 적으면서도 O(n²)에서 O(n)으로 성능을 끌어올릴 수 있는 가장 효율적인 방법입니다.

반응형
Comments