[2차 프로젝트 리팩토링] (1) : 쿼리스트링 중복 입력 문제 해결, 선택한 체크박스 유지되지 않는 문제 (전역상태관리)

GY·2022년 2월 26일
0

리액트

목록 보기
45/54
post-thumbnail

에러

checkPropTypes.js:20 Warning: Failed prop type: You provided a checked prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultChecked. Otherwise, set either onChange or readOnly.

이 에러가 콘솔창에 떠있었다.

쿼리스트링

  1. 쿼리스트링을 객체로 변환시키는 함수도 필요하다.
    지금은 무조건 append를 사용해서 선택한 값을 쿼리스트링으로 넣게만 되어있다. 이렇게 되면 단점이

  2. 중복된 쿼리스트링이 계속해서 입력된다.

    1. api가 없으니 별도로 이상한 데이터가 들어오지 않는 것이긴하지만...제대로 관리하는 건 아니니까.

      'category='게스트하우스'&adult=1&category='게스트하우스&'dult=1...무한반복'
  3. 유형별로 관리가 되는 것이 아니라, 각각을 쿼리스트링으로 넣었다. 예를 들어 숙박유형 = 게스트 하우스&호텔, 인원=성인1명&아이1명 과 같이 쿼리스트링이 들어가 항목별로 관리가 되어야 하는데, 숙박유형=게스트하우스, 숙박유형=호텔 과 같은 방식으로 들어간다. 그러다 보니 api에서 제대로 호출이 되지 않았다.* 백엔드에서 이 부분을 api로 어떻게 만들었는지는 잘 모르겠다. 하지만 api로 여러 유형을 선택한 것을 처리하려면 분명 이렇게 고쳐야 할 것이므로, 그리고 스테이폴리오에서도 이렇게 했기 때문에 방식을 그대로 따라해보기로했다.


category='게스트하우스&'호텔'이 아니라,
category='게스트하우스'&category='호텔'이 된다.
또 다른 문제는.. 선택한 값이 체크리스트에 반영되어 있지 않다는 것이다.

게스트 하우스만 체크했다면 다시 눌렀을 때 체크된 게스트 하우스를 해제하고 호텔을 체크할 수도 있어야 한다. 하지만 되지 않는 다는 것 ㅠㅠ

category=게스트하우스&호텔&아웃도어&adult=1에서 게스트하우스를 다시 눌러 체크해제 하면
category=호텔&아웃도어&adult=1이 되어야 하는데,
category=게스트하우스&category=호텔&category=아웃도어&adult=1에서
category=게스트하우스&category=호텔&category=아웃도어&adult=1&category='게스트하우스'가 된다.

왜 이렇게 됐을까?

  const handleFilter = stateObj => {
    const URLSearch = new URLSearchParams(location.search);
    Object.entries(stateObj).map(([key, value]) => {
      if (typeof value === 'boolean') {
        value && URLSearch.append('category', key);
      } else {
        value && URLSearch.append(key, value);
      }
    });
    navigate(`/list?` + URLSearch.toString());
    closeHandler();
  };

이렇게 관리하고 있었다.

  const [selectedType, setSelectedType] = useState({
    게스트하우스: false,
    호텔: false,
  });

  const handleChange = e => {
    const { name } = e.target;
    setSelectedType(current => ({ ...current, [name]: !current[name] }));
  };
  • 숙박 유형 외에 복수의 항목을 선택하는 다른 필터링 카테고리가 생긴다면 이 로직은 확장성이 없다. 바로 category라는 key값ㅇ르 명시해 넣어주므로 공통적으로 사용할 수 없을 뿐 아니라 조건이 value === boolean이기 때문에 어떤 곳에서 어떻게 쓰이는지 파악하기 힘들다.

체크박스의 선택/해제 상태를 관리하기 위해 상탯값을 boolean으로 정의했던 것 같다. (근데 왜 체크박스도 제대로 선택/해제가 되지 않았지..?)

            <li key={idx}>
              <label onChange={handleChange} name={item.name}>
                <span>{item.type}</span>
                <input
                  type="checkbox"
                  value="space"
                  name={item.name}
                  checked={selectedType[item.name]}
                />
              </label>
            </li>

바꿔보자.

일단 현재 코드에서 빠르게 위에서 언급한 문제만 고쳐보도록 하자.
그 외의 것들은 이후에 다시 리팩토링해보자.

  • append가 아닌 set을 사용했다.
    - URLSearchParams의 메서드 중 append는 단순히 키값을 추가하는 것이고, set은 해당하는 키의 값이 이전에 있었다면 제거한 뒤 추가한다. 따라서 계속해서 중복된 쿼리스트링이 추가되는 오류를 해결할 수 있다.
  • 복수의 선택지의 경우 하나의 key값으로 배열로 추가하였다. 따라서 &으로 연결한 다음 하나의 key로 묶여 쿼리스트링으로 들어가도록 만들었다.
  const handleFilter = stateObj => {
    const URLSearch = new URLSearchParams(location.search);
    Object.entries(stateObj).map(([key, value]) => {
      if (typeof value === 'array') {
        URLSearch.set(key, value.join('&'));
      } else {
        URLSearch.set(key, value);
      }
    });
    navigate(`/list?` + URLSearch.toString());
    closeHandler();
  };

선택할 때도 해당하는 이름이 배열에 없을 때만 넣어주도록 했다.

const [selectedType, setSelectedType] = useState({
    category: [],
  });

  const handleChange = e => {
    const { name } = e.target;
    if (!selectedType.category.includes(name)) {
      setSelectedType({
        ...selectedType,
        category: [...selectedType.category, name],
      });
    }
  };

정상적으로 쿼리스트링이 생성되고 업데이트 된다.

체크박스 선택이 유지되지 않는 문제

쿼리스트링은 정상적으로 작동하지만, 선택한 체크박스는 여전히 드롭다운 창을 닫았다가 다시 열었을 때 선택여부가 유지되지 않는다.

아마도 상위의 state가 업데이트되면서 해당 하위컴포넌트가 리렌더링되고, 선택한 체크박스의 상탯값이 초기화되는 것 같다.

전역 상태관리로 진행해보았다.
필터링 항목의 모든 값을 하나의 객체로 관리하고,

import { atom } from 'recoil';

export const filterConditionState = atom({
  key: 'filterConditionState',
  default: {
    city: '',
    count: {
      adult: 0,
      child: 0,
      baby: 0,
    },
    priceRange: {
      min: 0,
      max: 0,
    },
    category: [],
    theme: [],
  },
});

이 전역 상태에 각각의 필터링 항목에서 선택한 값을 업데이트 해주었다.
예를들면, 숙박인원을 선택할 때 인원을 추가하는 로직은 다음과 같다.

export default function SelectPeople({ closeHandler, handleFilter }) {
  const [filterCondition, setFilterCondition] =
    useRecoilState(filterConditionState);
  const { handleSearchParams } = useQueryString();

  const plusQuantity = name => {
    const updatedCount = {
      ...filterCondition.count,
      [name]: filterCondition.count[name] + 1,
    };
    setFilterCondition({
      ...filterCondition,
      count: updatedCount,
    });
  };

그리고 업데이트 된 전역 상탯값을 읽어와 value를 업데이트한다.

<InputNum>
  <input
	type="number"
	value={filterCondition.count[item.name] || 0}
	readOnly
    />
      <span></span>
</InputNum>

드롭다운창을 열고 닫아도 선택했던 체크박스가 그대로 유지된다!


아쉬운점...추가로 고민해봐야할 점

쿼리스트링 값이 스트링으로 연결되는 문제

  const handleSearchParams = (page, obj, type) => {
    Object.entries(obj).map(([key, value]) => {
      if (value.length) {
        if (type === 'multiple') {
          return URLSearch.set(key, value.join('&'));
        } else {
          return URLSearch.set(key, value);
        }
      }
    });
    navigate(`/${page}?` + URLSearch.toString());
  };

value들을 전부 &로 조인해버리니 쿼리스트링을 만드는 데는 문제가 없었지만, URLSearchParams만으로 쿼리스트링을 관리하기에 조금 부족함이 생겼다.
예를들어 보자.
{category:[guesthouse, hotel]}을 쿼리스트링으로 만들 때를 생각해보자.

  1. value를 join()을 사용해 guesthouse&hotel로 만들었다.
  2. 그리고 URLSearchParams 객체에 set메서드를 활용해 넣어주었다. URLSearchParams.set('category', 문자열)
  3. 이후에 getAll()로 각 파라미터를 가져오려면 category의 파라미터는 [guesthouse, hotel]이어야 관리하기 편하겠지만 guesthouse&hotel이라는 연결된 문자열을 가져오게 된다.

따라서 현재 선택된 값들, 즉 쿼리스트링의 값들을 읽어와 사용해야 하는 경우가 생긴다면 이렇게 연결된 문자열을 분리해 각 값을 관리하는 로직이 별도로 필요하다.

아직 이 로직이 필요하지는 않지만, 확장성 있는 코드를 위해 여유가 된다면 만들어주는 것도 좋을 것 같다.

선택한 항목의 상태값을 객체로 관리할 필요가 있을까?

기존 코드가 선택한 값을 객체로 관리한 뒤 해당 객체를 쿼리스트링으로 변환시켜주었는데, 이게 꼭 필요할지 의문이 든다. 객체는 key에 해당하는 value를 구분해 넣을 수 있다는 장점이 있는데, 독립된 컴포넌트에서 관리하는 상탯값은 하나의 key안에 포함되는 value들이다.

따라서 value만 적절한 형태로 관리하고, 쿼리스트링으로 변환할 때 하나의 key를 인자로 전달해주면 충분할 것 같다.

전역 상태관리가 꼭 필요할까?

아무래도 생각했던 대로 상위 컴포넌트의 state가 리렌더링되면서 하위 컴포넌트의 state가 초기화되어 선택한 체크박스가 드롭다운 창을 닫았다 열었을 때 유지되지 않는 것이 맞는 것 같다.
전역 상태관리로 문제를 해결하긴 했지만, 좋은 방법은 아닌 것 같다.
드롭다운창을 닫았을 때 부모컴포넌트에서 업데이트되는 state가 있는지 확인하고, 다시 해결해봐야겠다.

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글