리액트에서 카테고리를 구현해 보았다.

octofox·2022년 12월 19일
0

구현할 카테고리 기능 스크린샷

하나의 카테고리에는 또 다른 카테고리 정보가 들어있다.
카테고리 마다 그 깊이가 다르다. 어떻게 이 정보들을 유연하게 표현할 수 있을까?

class Category {
	cateNo: number;
	name: string;
	subCate: [] | Category[];
}

구조를 간단하게 클래로 표현해 보았다.
보면 정보가 재귀의 형태를 취하고 있다. subCate는 다시 Category가 된다.

재귀라는 옵션이 머리에 없던 처음에는 카테고리 트리의 마지막 깊이를 확인하고 그 만큼의 useState를 사용하였다. 1번째를 고르는, 2번째, 3번째 ... 이렇게 해서 5개 까지 만들고 컴포넌트도 5개를 정적으로 만들어서 조회 해야할 state가 너무나 많았다. 그리고 다른 더 깊은 카테고리와는 호환되지 않는 정적 컴포넌트들은 재사용되기 어려웠다.
쿠팡, 11번가, 아마존, 알리익스프레스 전부 다른 카테고리들을 가지고 있다.
이것들의 레벨은 물론 다 다르다.

그래서 재귀함수를 사용한 컴포넌트를 만들었다.

컴포넌트들의 종류는 이렇다.
1. 카테고리 컨테이너
2. 카테고리 재귀 함수
3. 카테고리 박스
4. 카테고리 리스트 아이템

트리의 특정 노드를 가리킬 수 있는 효과적인 구조가 무엇일까 생각했다.
트리 노드의 위치는 레벨과 부모 cateNo로 결정된다.
그래서 배열에 cateNo를 모은 형태로 노드를 가리키기로 했다.
배열의 길이는 트리의 레벨이 된다.
배열의 cateNo들은 그 부모들이 누군지 알려준다.

이 정보들을 재귀함수에 넘겨주어서 카테고리 박스를 그리도록 했다.

재귀함수는 이렇게 생겼다.

import CategoryBox from './CategoryBox'

type CategoryRecursionProps = {
  categoryList: any[]
  treePath: any[]
  setTreePath: any
  level: number
}

const CategoryRecursion = ({
  categoryList,
  treePath,
  setTreePath,
  level,
}: CategoryRecursionProps) => {

  function descendCateIndex(categoryList: any[], cateNo: number) {
    return categoryList.findIndex((cate) => cate.cateNo == cateNo)
  }

  return (
    <>
      <CategoryBox
        key={level}
        level={level}
        categoryList={categoryList}
        setTreePath={setTreePath}
      />
      {level < treePath.length ? (
        <CategoryRecursion
          categoryList={
            categoryList[descendCateIndex(categoryList, treePath[level])]
              .subCate
          }
          treePath={treePath}
          setTreePath={setTreePath}
          level={level + 1}
        />
      ) : null}
    </>
  )
}

export default CategoryRecursion

특정 컴포넌트를 반복하기 위해 구조적인 컴포넌트로 CategoryRecursion을 만들었다.
CategoryRecursion은 CategoryBox를 재귀한다.
재귀해서 들어갈 때 레벨은 당연히 1씩 증가한다.
표현되는 카테고리의 레벨은 cateNo로 구성된 배열의 길이에 의해서 제한된다.

import { Col } from 'react-bootstrap'
import CategoryItem from './CategoryItem'

interface CategoryBoxProps {
  categoryList: any[]
  setTreePath: any
  level: number
}

const CategoryBox = ({
  categoryList,
  setTreePath,
  level,
}: CategoryBoxProps) => {
  return categoryList.length != 0 ? (
    <Col>
      {categoryList.map((category, i) => (
        <CategoryItem
          key={i}
          level={level}
          category={category}
          setTreePath={setTreePath}
        />
      ))}
    </Col>
  ) : null
}

export default CategoryBox

카테고리 아이템은 이렇다.

import { ListGroup } from 'react-bootstrap'

interface CategoryItemProps {
  category: any
  setTreePath: any
  level: number
}

const CategoryItem = ({ category, setTreePath, level }: CategoryItemProps) => {
  return (
    <ListGroup.Item
      onClick={() => {
        setTreePath((prev: any[]) => {
          if (prev.length > level) {
            return [...prev.slice(0, level), category.cateNo]
          } else {
            return [...prev, category.cateNo]
          }
        })
      }}
    >
      {category.name}
    </ListGroup.Item>
  )
}

export default CategoryItem

선택된 아이템 노드의 레벨이 현재 표현되고 있는 마지막 레벨보다 작다면 그 위치의 카테고리 정보를 배열에서 제거한다.
반대로 선택된 아이템 노드의 레벨이 현재 표현되고 있는 마지막 레벨과 같다면 배열에 cateNo를 추가한다.

profile
개발자라고 우기는 노답 소년

0개의 댓글

Powered by GraphCDN, the GraphQL CDN