ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 카테고리 버튼 상태관리 / props 분리
    코드잇 2024. 6. 13. 21:48

    서문

    이전의 포스팅 중 하나인 [BLOB] 프로젝트 회고의 프로젝트에서 카테고리 버튼 상태관리를 구현하면서 발생한 문제와 이를 해결한 방식을 담고 있습니다.

     

    카테고리 버튼 상태관리

    왼쪽) 글쓰기 모달, 오른쪽) 필터링 모달

     

    (문제)

    1. 메인 카테고리의 화살표 상태는 최근에 클릭한 메인 카테고리 하나만 true로 바뀌어야 한다.

    서브 카테고리는 메인 카테고리의 종류에 따라서 색상과 상태가 바뀐다. 예를 들어 '추천' 카테고리에서 서브 카테고리인 '음식점'을 클릭한 후, '비추천' 카테고리를 클릭하면 화면 상에서는 서브 카테고리가 아무것도 클릭되지 않은 상태가 되어야 한다. 

     

    2. 카테고리 버튼은 글쓰기 모달과 필터링 모달에서 사용된다. 글쓰기 모달은 카테고리 버튼이 단일 선택만 가능하지만, 필터링 모달에서는 중복 선택이 가능하다.

     

     

    (해결책 도출)

    1. 메인 / 서브 카테고리 버튼을 하나의 상태로 관리한다.

     

    - 서브 카테고리는 메인 카테고리에 포함이 되면서 색상이 바뀌기 때문에, 메인 / 서브 카테고리는 서로 연결되어 있어야 한다.

    - 백엔드에게 데이터를 보낼 때 '메인 카테고리 이름:서브 카테고리 이름' ( ex) RECOMMEND:WEATHER ) 형식으로 서로 연결된 데이터를 보내야 한다.

     

    2. 카테고리 버튼을 중복 / 단일 선택 가능할 수 있도록 props 분리한다.

     

     

    (시행)

    1. 메인 / 서브 카테고리 버튼을 하나의 상태로 관리한다.

    const initialCategoriesWithSub = categories.map((category) => ({
      name: category,
      isSelectedCategory: false,
      isSelectedArrow: false,
      subCategories: subCategories.map((subCategory) => ({
        name: subCategory,
        isSelectedSubCategory: false,
      })),
    }));

    map 메소드를 이용해서 메인 / 서브 카테고리의 기본값을 정의했다.

     

     

     

     

    2. 카테고리 버튼을 중복 / 단일 선택 가능할 수 있도록 props 분리한다.

    // FilterModal.tsx
    
    export default function FilterModal() {
    
    const [categoriesWithSub, setCategoriesWithSub] = useState(initialCategoriesWithSub);
    
    <CategoryList
      categoriesWithSub={categoriesWithSub}
      setCategoriesWithSub={setCategoriesWithSub}
      filterType='FEED'
    />
    ...
    
      <div className={cx('button-box')}>
        <BlobButton text='BLOB' type='submit' color='button-colord-contain' onClick={submitFilteredData} />
      </div>
    }

     

    filterType props로 필터링 모달은 피드페이지에서만 쓰이기 때문에 'FEED', 글 쓰기 모달은 'WRITE'로 분리시켰다. 

     

    필터링 모달에 제출 버튼이 있으니 카테고리 상태는 필터링 모달에서 관리했다. CategoryList 컴포넌트를 생성 후 카테고리 상태를 props로 내려줬다.

     

     

     

    export default function CategoryList({ categoriesWithSub, setCategoriesWithSub, filterType }: CategoryListProps) {
          {categories.map((category, categoryIndex) => (
            <FilterCategoryButton
              key={category}
              index={categoryIndex}
              filterType={filterType}
              categoriesWithSub={categoriesWithSub}
              setCategoriesWithSub={setCategoriesWithSub}
            >
              <div className={cx('sub-category-list')}>
               ...
              </div>
            </FilterCategoryButton>
          ))}

     

    CategoryList에서 메인 / 서브 카테고리 버튼을 렌더링 해주었다. 먼저 메인 카테고리 버튼의 상태관리부터 먼저 작업했다.

     

     

     

    export default function FilterCategoryButton({
      children,
      index,
      filterType = 'FEED',
      categoriesWithSub,
      setCategoriesWithSub,
    }: FilterCategoryButtonProps) {
    
    const { name, isSelectedCategory, isSelectedArrow } = categoriesWithSub[index];
    
     const handleClickCategory = () => {
        setCategoriesWithSub((prev) => {
          const prevCategories = [...prev];
    
          for (const prevCategory of prevCategories) {
            resetSubCategories(prevCategory);
    
            if (filterType === 'FEED' && prevCategory.name === name) {
              const toggle = !prevCategory.isSelectedCategory;
              prevCategory.isSelectedCategory = prevCategory.isSelectedArrow = toggle;
            } else if (filterType === 'FEED' && prevCategory.name !== name) {
              prevCategory.isSelectedArrow = false;
            } else if (filterType === 'WRITE') {
              const isSelected = prevCategory.name === name && !prevCategory.isSelectedCategory;
              prevCategory.isSelectedCategory = prevCategory.isSelectedArrow = isSelected;
            }
          }
    
          return prevCategories;
        });
      };
      
        const resetSubCategories = (category: CategoryState) => {
          if (!category.isSelectedCategory) {
            for (const subCategory of category.subCategories) {
              subCategory.isSelectedSubCategory = false;
            }
          }
      };
    
    ...
    
    <button
        type='button'
        onClick={handleClickCategory}
        className={cx(
          'background',
          {
            [`button-clicked-color-${name}`]: isSelectedCategory,
          },
          `filtering-type-${filterType}`,
        )}
      >

     

    메인 카테고리 버튼 컴포넌트이다. 카테고리 버튼 하나의 상태만 관리하기 위해 index를 활용했고 구조분해 할당으로 상태를 나누었다.

     

    버튼 렌더링 부분에서는 버튼 클릭시 handleClickCategory 이벤트 핸들러가 실행된다. for문으로 메인 카테고리를 하나씩 순회하면서 선택한 카테고리의 색상과 화살표 상태를 바꾸어주었고 선택되지 않은 나머지 카테고리의 화살표의 상태는 false로 리셋되게 해주었다. 그리고 if 문으로 filterType이 'FEED' 이면 중복선택 'WRITE'이면 단일 선택 가능하게 구현하였다. 

     

    만약 메인 카테고리 버튼이 클릭 된 상태에서 한번 더 클릭하면 서브 카테고리 상태도 모두 클릭되지 않은 상태로 리셋된다.

     

     

     

     

    export default function FilterSubCategoryButton({
      categoryIndex,
      index,
      filterType = 'FEED',
      categoriesWithSub,
      setCategoriesWithSub,
    }: FilterSubCategoryButtonProps) {
      const { name: categoryName } = categoriesWithSub[categoryIndex];
      const { name: subCategoryName, isSelectedSubCategory } = categoriesWithSub[categoryIndex].subCategories[index];
    
      const handleClickSubCategory = () => {
        setCategoriesWithSub((prev) => {
          const prevCategories = [...prev];
    
          for (const subCategory of prevCategories[categoryIndex].subCategories) {
            if (filterType === 'FEED' && subCategory.name === subCategoryName) {
              subCategory.isSelectedSubCategory = !subCategory.isSelectedSubCategory;
            } else if (filterType === 'WRITE') {
              subCategory.isSelectedSubCategory = false;
    
              if (subCategory.name === subCategoryName) {
                subCategory.isSelectedSubCategory = !subCategory.isSelectedSubCategory;
              }
            }
          }
    
          return prevCategories;
        });
      };

     

    서브 카테고리도 메인 카데고리와 비슷한 형식으로 작성했다. 서브 카테고리에는 화살표 상태가 없기 때문에 어렵지 않게 구현할 수 있었다.

     

     

     

    (회고 및 개선)

     

    한번에 다 해결하려고 하지말고 하위 컴포넌트와 상태부터 먼저 해결해 나가는게 키포인트라고 생각했다. 그래서 필터링 모달에 메인 카테고리 버튼을 먼저 구현하고, 서브 카테고리 버튼을 구현하였다. 필터링 모달을 구현한 이후에, 글쓰기 모달에 카테고리 버튼을 구현해나갔다.

     

    props에 따라서 if 문으로 다르게 처리해주었는데, 메인 카테고리 버튼의 화살표 클릭 여부에 서브 카테고리가 보여지기 때문에 코드가 더 길어지고 분기 처리가 어려웠던 것 같다.

     

     

    댓글

Designed by Tistory.