숙박 플랫폼 리팩토링 - 검색 페이지 useQueryString으로 공통로직 재사용하기

GY·2022년 3월 11일
0

리액트

목록 보기
50/54
post-thumbnail

지난번 포스팅에 이어, 리팩토링을 완료한 부분을 정리해보았다.

이 프로젝트는 다양한 검색 항목으로 원하는 숙박장소를 검색하는 것이 핵심 기능이었다.
내가 맡았던 부분은 아니었지만 중요했던 만큼 꼼꼼히 돌아보았는데,
프로젝트가 끝난 후 기존 코드는 쿼리스트링이 중복 입력되는 등의 문제가 있었기 때문에 수정이 필요했다.

  1. 처음엔 리팩토링 과정에서 선택한 필터링 항목이 유지되지 않는 문제가 발생했고, 이를 해결하기 위해 전역상태관리를 사용했다.

👉 어떤 문제였을까? 발생했던 문제와 전역상태관리를 사용했던 이유 보러가기

  1. 이후 이 부분이 아쉬워 다시 코드를 살펴보고 문제의 원인을 찾아내었고, 전역상태관리 없이 쿼리스트링만을 사용해 컴포넌트 내부에서 독립된 상태관리를 하도록 로직을 변경했다.

👉 문제의 원인은 무엇이었을까? 문제해결과정 보러가기

  1. 그리고 각각의 형태에 맞추어 2개 이상의 컴포넌트에서 재사용되는 로직을 커스텀훅으로 작성했는데, 오늘은 이 부분에 대해 정리해보았다.

useQueryString 커스텀훅 만들기

커스텀 훅을 사용하기로 결정한 이유는 다음과 같다.

  1. 체크박스의 체크 여부를 실시간으로 반영하기 위해서는 리렌더링이 되도록 useState를 사용한 상태관리를 해야한다.
  2. 단순히 utils 함수를 사용하게 되면 객체state를 업데이트 하기 위한 로직이 중복되며, 컴포넌트 안의 코드도 길어지게 된다.

따라서 useQueryStringObject를 따로 만들어 state까지 관리하고자 했다. 객체 값을 변경하는 로직도 함수로 만들어 코드 재사용률을 높이는 것이 목표였다.

useQueryStringObject는 상태를 객체형태로 관리해야 할 경우 사용하는 로직을 담은 커스텀 훅이다.
다음과 같이 인원과 가격을 각각의 key, value형태로 저장해야 하는 경우 사용했다.

숙박 인원숙박 가격
count = {
  adult:0,
  child:0,
  baby:0,
}
priceRange = {
  min:0,
  max:0,
}

1. useQueryStringObject

쿼리스트링 상탯값 관리

기본적으로 URLSearchparams를 이용해 현재 쿼리스트링을 받아와야 하고, 상탯값을 새로 선언해주어야 하므로 이 부분을 중복으로 사용하지 않기 위해 커스텀 훅 내부에서 정의해주었다.

export const useQueryStringObject = (objKey, stateObj) => {
  const { search } = useLocation();
  const navigate = useNavigate();
  let URLSearch = new URLSearchParams(search);
  const [selectedListObject, setSelectedListObject] = useState(stateObj);

상탯값 변경 함수

객체 불변성을 지키면서 상탯값을 업데이트하다 보면 유사한 로직이 반복되어 코드의 길이가 길어지게 된다. 따라서 하나의 함수로 선언해 재사용하도록 했다.

  const addFilterObject = (name, updatedResult) => {
    const updatedObject = {
      ...selectedListObject,
      [name]: updatedResult,
    };
    setSelectedListObject(updatedObject);
  };

컴포넌트 내부에서 어떻게 사용했는지 알아보자

숙박 인원

숙박 인원을 선택하면 count라는 키값으로 객체의 각각의 key, value값을 문자열로 만들어 넣어줄 것이다.
예를 들어 성인1명, 청소년 2명을 선택했다면 count=adult%D21&child=2가 된다.
전체 쿼리스트링의 key값은 'count'가 될 것이므로 첫번재 인자로 넣어준다. 그리고 두번째 인자로 초기 값이 될 객체를 넣어준다. 이 객체를 굳이 정의해서 넣어주는 이유는, 초기값이 빈 객체일 경우 인원을 증감함에 따라 숫자를 더하고 뺄 수 없기 때문이다. 값이 NaN으로 반환되지 않도록 미리 초기 값을 정의해준다.

export default function SelectPeople() {
  const initialState = {
    adult: 0,
    child: 0,
    baby: 0,
  };
  const { addFilterObject, selectedListObject, parseObjectToSearchParams } =
    useQueryStringObject('count', initialState);

인원 증감하기

객체 state를 변경하는 로직은 계속해서 중복이 될 뿐더러 코드의 길이가 길어져 로직을 따로 분리해 재사용했다.
useQueryStringObject에서 addFilterObject 함수는 객체의 key값과 변경할 값 또는 콜백함수를 전달받는다.

//useQueryStringObject.js
  const addFilterObject = (name, updatedResult) => {
    const updatedObject = {
      ...selectedListObject,
      [name]: updatedResult,
    };
    setSelectedListObject(updatedObject);
  };

이 로직을 사용하는 컴포넌트는 다음과 같이 간결하게 코드를 작성해 state를 변경할 수 있다.

//selectPeople.js
  const onClickPlusButton = name => {
    addFilterObject(name, selectedListObject[name] + 1);
  };

  const onClickMinusButton = name => {
    if (selectedListObject[name]) { // 인원이 0이상일 때만 감소
      addFilterObject(name, selectedListObject[name] - 1);
    }
  };

숙박 가격

가격은 min과 max값을 key로 가지는 객체여야 한다.
쿼리스트링의 key값은 priceRange로, 다음과 같은 형태로 쿼리스트링을 관리하고자 했다.
priceRange=min%D22000&max%D228000

export default function SelectPrice({ closeHandler }) {
  const initialState = {
    min: 0,
    max: 0,
  };
  const { addFilterObject, selectedListObject, parseObjectToSearchParams } =
    useQueryStringObject('priceRange', initialState);

슬라이더 바를 옮길 때마다 가격 업데이트 하기

마찬가지로 슬라이더 바를 옮길 때마다 가격을 업데이트 해주어야 했다.
똑같이 addFilterObject 함수를 사용하였다.

  const handleChange = e => {
    const maxPrice = e.target.value * 10000;
    addFilterObject('max', maxPrice);
  };

객체와 쿼리스트링 변환 함수

객체를 쿼리스트링으로 변환하는 함수와, 쿼리스트링을 객체로 변환하는 함수 2개를 커스텀 훅 내부에 구현하여 사용할 수 있도록 했다.

객체를 쿼리스트링으로 변환

  const parseObjectToSearchParams = (
    obj = selectedListObject,
    page = 'list'
  ) => {
    let valueArr = [];
    Object.entries(obj).map(([key, value]) => {
      if (value) {
        valueArr.push(key + '=' + value);
      }
    });
    URLSearch.set(objKey, valueArr.join('&'));
    navigate(`/${page}?` + URLSearch.toString());
  };

쿼리스트링을 객체로 변환

  const parseQueryIntoObject = querystring => {
    const params = new URLSearchParams(querystring);
    const obj = {};

    for (const key of params.keys()) {
      if (params.getAll(key).length > 1) {
        obj[key] = params.getAll(key);
      } else {
        obj[key] = params.get(key);
      }
    }

    return obj;
  };

이 함수들을 어떻게 사용했을까?


숙박 기간 선택시 쿼리스트링 업데이트

숙박 기간을 설정할 때 라이브러리를 사용해 각각 시작 날짜와 끝날짜를 선택할 때마다 상탯값을 업데이트해주어야 했다. 이 때 라이브러리 특성상 객체로 감싸 각각의 상탯값을 변경해줄 수 없었고 무한 루프에 빠지게 되는 문제가 있었다.

일반 객체를 사용해 쿼리스트링에서 객체로 변환해준 뒤 값을 업데이트 해주고, 다시 쿼리스트링으로 변환해 입력하는 방식을 사용했다.

이 때 useQueryStringObject에 포함된 함수 parseObjectToSearchParams와 parseQueryIntoObject를 사용했다.

  const { parseObjectToSearchParams, parseQueryIntoObject } =
    useQueryStringObject('dates', datesObj);

  useEffect(() => {
    const newDates = parseQueryIntoObject(dates);
  }, [location.search]);
  const handleDatesChange = ({ startDate, endDate }) => {
    const queryString = URLSearch.get('dates');
    const queryObject = parseQueryIntoObject(queryString);
      parseObjectToSearchParams(queryObject);
  };

2. useQueryStringArr

숙박 유형과 숙박 테마는 동일하게 체크리스트로 이루어져있다.
따라서 선택한 항목을 배열헝태로 넣고 빼면서 체크박스 상태를 관리하는 것이 적절하다고 생각했다.

숙박 유형숙박 테마
selectedType = ['guesthouse']selectedTheme = ['pool', 'designToor']

숙박 유형

export default function SelectType() {
  const {
    addFilterArr,
    handleCheckedAll,
    isCheckedAll,
    isChecked,
    parseArrayToSearchParams,
  } = useQueryStringArr('category');

체크박스를 클릭할 때마다 선택한 값이 배열에 추가된다. 이 때 이미 배열에 선택한 값이 있으면 배열에서 값을 다시 삭제하고, 없을 경우에만 추가하도록 한다.

//useQueryStringArr.js
  const addFilterArr = e => {
    let updatedList = [];
    const { name } = e.target;
    if (!selectedList.includes(name)) {
      updatedList = [...selectedList, name];
    } else {
      updatedList = [...selectedList].filter(cate => {
        return cate !== name;
      });
    }
    setSelectedList(updatedList);
  };

체크박스에는 isChecked함수로 체크여부를 반영하고, 체크박스가 클릭될 때마다 handleChange함수를 실행한다.

//selectType.js
{TYPE_DATA.category.map((item, idx) => {
  return (
    <input
      type="checkbox"
      checked={isChecked(item.name)}
      onChange={handleChange}
      />

체크박스를 클릭할 때마다 addFilterArr함수를 실행해 선택한값을 배열에 넣거나 빼도록 한다.

//selectType.js
  const handleChange = e => {
    addFilterArr(e);
  };

선택/전체 선택 여부

  const handleCheckedAll = () => {
    let updatedList = [];

    if (!isCheckedAll()) {
      TYPE_DATA[objKey].forEach(obj => {
        updatedList.push(obj.name);
      });
    }

    setSelectedList(updatedList);
  };

  const isCheckedAll = () => {
    return TYPE_DATA[objKey].every(obj => selectedList.includes(obj.name));
  };

  const isChecked = property => {
    return selectedList.includes(property);
  };

선택 항목 적용하기, 쿼리스트링으로 변환하기

커스텀 훅에서 전달받은 key값, 즉 'cagetory'를 key로 해 업데이트 된 배열 상탯값을 쿼리스트링으로 변환한다음 적용한다.

//useQueryStringArr.js
  const parseArrayToSearchParams = (page = 'list') => {
    URLSearch.set(objKey, selectedList.join('&'));
    navigate(`/${page}?` + URLSearch.toString());
  };

컴포넌트에서는 간단하게 사용할 수 있다.

//selectType.js
  const onClickApplyButton = () => {
    parseArrayToSearchParams();
  };

숙박 테마

같은 유형의 검색 항목이므로 숙박 유형과 동일하게 사용할 수 있다.

export default function SelectTheme() {
  const {
    addFilterArr,
    handleCheckedAll,
    isCheckedAll,
    isChecked,
    parseArrayToSearchParams,
  } = useQueryStringArr('theme');

  const handleChange = e => {
    addFilterArr(e);
  };

  const onClickApplyButton = () => {
    parseArrayToSearchParams();
  };

체크박스

                <span>전체</span>
                <input
                  type="checkbox"
                  value="space"
                  name="all"
                  checked={isCheckedAll()}
                  onChange={handleCheckedAll}
                />
              </label>
            </li>
            {TYPE_DATA.theme.map((obj, idx) => {
              return (
                <li key={idx}>
                    <input
                      type="checkbox"
                      checked={isChecked(obj.name)}
                      onChange={handleChange}
                    />
                  </label>

3. useQueryString

숙박 지역

function SelectCity({ onToggle }) {
  const { selectedState, setSelectedState, parseStringToSearchParams } =
    useQueryString('city');

  function onClickSearch() {
    onToggle();
    parseStringToSearchParams();
  }

  return (
    <ModalSelectForm
      title="어디로 떠날까요?"
      onClickSearch={onClickSearch}
      onToggle={onToggle}
    >
      <Location
        selectedCity={selectedState}
        setSelectedCity={setSelectedState}
      />
    </ModalSelectForm>
  );
}

더 고민해보고 싶은 점

useQueryStringObject: 하나의 쿼리스트링을 다시 한번 감싸 key,value형태로 넣은 부분

예를 들어, checkin=2022-03-22&checkout=2022-03-25형태로 넣었던 쿼리스트링을 리팩토링과정에서 수정했는데, 위의 코드대로 하면
dates=checkin%D2=2022-03-22&checkout%D22022-03-25와 같은 형태가 된다. 즉, dates라는 key로 객체를 쿼리스트링으로 변환한 문자열이 한번더 감싸진 것이다.

여기까지 리팩토링을 진행한 로직으로 만든 쿼리스트링은 기존의 문제점을 해결하였지만 프로젝트 당시 백엔드와 협의한 내용과 조금 다르다. 따라서 이 로직은 쿼리스트링 관리에는 문제가 없지만 프로젝트 내부에서는 api를 호출할 때 에러가 난다.

그럼... 왜 이렇게 만들었지?

어떻게 쿼리스트링을 효율적으로 관리할 수 있을 것인지에 대한 고민이 필요했기 때문이다. 직접 해보지 않으면 와닿지 않는 부분들이 있었기 때문에, 한번 만들어보고 직접 비교해보고 싶었다.

지금은 dates=checkin%D2=2022-03-22&checkout%D22022-03-25와 같은 형태로 쿼리스트링을 생성하지만, 백엔드와 협의했던 형태는 checkin=2022-03-22&checkout=2022-03-25이었다. 아마 기획에서 참고했던 스테이폴리오 홈페이지도 이와 같이 쿼리스트링을 관리하는 것으로보아 굳이 한번더 감쌀 필요가 없었거나, 백엔드 api 요청에 적합한 형태가 이것이기 때문에 선택한 것 같다.

프로젝트 자체의 완성도를 위해 기존 협의했던 대로 쿼리스트링을 수정하고, 전자의 형태도 백엔드에서 처리할 수 있는지 알아보려고 한다.

그리고 나면 커스텀 훅을 다시 더 간결하게 수정할 수 있을 것 같다!

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

0개의 댓글