장바구니 아이템을 모두 삭제했는데, 전체 선택 박스가 체크 되어 있어요!

이나리·2022년 6월 25일
0

문제의 코드

// 삭제 버튼 클릭시, 발생할 수 있는 이벤트
dispatch({ type: 'REMOVE_CART_ITEM', item }); // 단일 아이템 개별 삭제
dispatch({ type: 'REMOVE_SELECTED_CART_ITEM', item }); // 복수 아이템 선택 삭제

function cartReducer(state, action) {
  switch (action.type) {
    case 'REMOVE_CART_ITEM':
      return {
        ...state,
        allSelected: state.cart
          .filter(
            item =>
              item.name !== action.item.name || item.size === action.item.size
          )
          .every(item => item.selected),
      };
    case 'REMOVE_SELECTED_CART_ITEM':
      return {
        ...state,
        allSelected: state.cart
          .filter(
            item =>
              item.name !== action.item.name || item.size === action.item.size
          )
          .every(item => item.selected),
      };
  }
}

위 코드는 장바구니의 아이템을 1개씩 삭제하거나, 혹은 여러개를 선택하여 삭제할 때 실행되는 리듀서 함수 코드입니다.
액션이 디스패칭될 때, 이 코드는 1가지 상황을 제외하고 잘 작동합니다.
장바구니 리스트에서 모든 아이템을 삭제할 때를 제외하고 말이죠!

장바구니에 아이템이 존재하지 않는다면, 아이템 자체가 없으므로 전체 선택 박스는 무조건 해제가 되어야 합니다.
그러나, 이 코드는 무조건 선택된 상태를 만듭니다. 무엇이 잘못된 걸까요?

먼저, 로직에 사용된 메서드에 대해 살펴보겠습니다.

Array.prototype.every(callbackFn)

every 메서드의 동작 방식은 다음과 같습니다.

콜백함수가 해당 배열에 대해 false를 리턴하는 요소를 찾으면 그 즉시, false를 리턴합니다.
어떤 요소도 false를 리턴하지 않을 경우, true를 리턴합니다.
즉, false를 찾을 때까지 배열을 반복합니다.

그런데 빈 배열에서 호출된다면 어떻게 될까요?
어떤 요소도 false를 리턴하지 않으므로, 항상 true를 리턴합니다.

코드를 다시 살펴보겠습니다.

state.cart
  .filter(
    item => item.name !== action.item.name || item.size === action.item.size
  )
  .every(item => item.selected);

every 메서드를 실행하기 앞서, 실행되는 filter 메서드는 삭제되지 않을 요소들을 담은 배열을 리턴합니다.
만약 총 1개 중에 1개를 삭제하거나, 2개 중 2개를 모두 선택해서 삭제 하려고 한다면, 이 배열은 빈 배열이 될 것입니다.
여기서 문제가 발생하는 것이죠. 빈 배열에 대해서 every 메서드는 항상 true를 리턴한다고 되어 있으니, 전체 선택 상태가 항상 true가 됩니다.


문제 해결 시도

사용한 메서드를 통해 해당 버그의 원인을 찾을 수 있었습니다.
하지만 이 버그를 어떻게 해결할 수 있을까요? 다른 메서드를 사용해야 할까요?

다른 메서드를 이용하는 방법은 있을 것 같지만, every 메서드만큼 간단하게 표현해내기는 어려울 것 같습니다.

그렇다면, every 메서드를 유지하기 위한 방법을 찾아보겠습니다.
먼저, 버그의 원인은 filter 메서드가 리턴하는 배열이 빈 배열이기 때문입니다.
이 배열이 빈 배열이 나오는 경우를 계산해보면,

filter 메서드는 삭제하려고 선택한 아이템을 제외한 아이템들만 리턴합니다.
즉, 빈 배열을 리턴한다 = 아이템이 모두 선택된 상태 를 나타냅니다.

사용자가 장바구니 아이템을 삭제하려고 할 때는, 2가지의 액션이 가능합니다.
1. 아이템별 개별 삭제 (action type: REMOVE_ITEM)
2. 복수 아이템 선택 후 삭제 (action type: REMOVE_SELECTED_ITEM)

1번의 경우는, 전체 아이템이 1개밖에 없을 때 해당 아이템마저 삭제하려고 한다면 빈 배열을 리턴할 것입니다.
2번의 경우는, 전체 아이템을 모두 선택하고 삭제하면 빈 배열을 리턴하겠죠?

위 경우를 고려하여, 코드를 변경하면 됩니다.
filter 메서드가 빈 배열이 나오지 않도록 제어하는 것은 불가능하기 때문에, filter 메서드 전에 조건문을 추가합니다.
해당 조건에는 filter 메서드를 실행하지 않고 false를 리턴하도록 강제하면, 빈 배열일 경우에는 항상 false가 리턴되어 전체 선택 버튼이 해제 상태가 될 것입니다.


완성된 코드

function cartReducer(state, action) {
  switch (action.type) {
    case 'REMOVE_CART_ITEM':
      return {
        ...state,
        allSelected:
          state.cart.length - 1 > 0
            ? state.cart
                .filter(
                  item =>
                    item.name !== action.item.name ||
                    item.size !== action.item.size
                )
                .every(item => item.selected)
            : false,,
      };
    case 'REMOVE_SELECTED_CART_ITEM':
      return {
        ...state,
        allSelected:
          state.cart.filter(item => item.selected).length === state.cart.length
            ? false
            : state.cart
                .filter(item => !item.selected)
                .every(item => item.selected),
      };
  }
}

더 쉽게 해결 가능한 코드

조건부 렌더링 코드를 작성하게 되면, 위 버그를 신경 쓰지 않아도 문제가 되지는 않습니다.

하지만, 상태를 기반으로 렌더링하기 때문에 정확한 상태값을 전달할 필요가 있기 때문에 위와 같은 코드를 작성하는 것이 더 좋다고 생각합니다.

function Cart() {
  return <>{cart.length > 0 ? <RenderCartList /> : <RenderNone />}</>;
}

0개의 댓글