프로젝트 기록/오름마켓

[개발일지02/🏔️오름마켓 React] useSearchParams로 필터 Query String 구현하기

now() 2024. 3. 22. 22:16
 🙌 리팩토링을 통해 프론트엔드에서 처리되던 정렬 및 필터 기능을 API와 연결하는 작업을 진행했다. 렌더링 된 버튼의 값과 실제 API 요청을 위해 전달해 줘야 했던 Query String의 값이 달라 두 값을 처리해 줘야 했는데, 이 과정에서 많이 헤맸던 기억을 잊지 않고자 블로그에도 짧게 기록을 남겨본다.

 

📑 요구 사항

간단하게 어떤 기능을 구현해야 했는지 정리해 보자.

1) UI/UX

2) 기능

  • 초기 렌더링시 정렬 '최신순' 버튼만 기본값으로 선택되어 있으며, 정렬 버튼은 단일 선택만 가능하다.
  • 필터 버튼은 각 조건별로 단일 선택만 가능하지만, 세 옵션은 중복 조합이 가능하다.
  • 정렬과 필터 버튼이 눌리면 API GET 요청을 통해 데이터를 받아온다.

3) API 

  • 정렬과 필터의 카테고리는 DB코드로 조회가 가능하며, 가격과 배송비는 min, max 범위로 조회가 가능하다.

이를 위해 작업하며 시행착오를 겪었던 조건은 아래와 같았다.

  • API 요청 쿼리에 맞춰 필터의 가격과 배송비의 범위를 min, max로 처리해 준다.
  • 필터의 '전체'를 클릭한 경우 Query String에서 해당되는 옵션의 파라미터를 삭제해 준다.
  • 새로고침이 되면 기존 정보가 유지되도록 현재 url의 변경되는 Query String 정보를 체크해 준다.

 

1️⃣ 클릭한 버튼 옵션에 따른 데이터 처리

필터의 가격과 배송료 버튼은 상수 데이터를 만들어 렌더링 해주고, 선택된 옵션에 대한 금액 범위를 처리하는 상수 데이터를 활용하여 데이터를 필터링해주고 있었다.

// 상수 데이터
전체: { min: 0, max: Infinity },
'1만원 이하': { min: 0, max: 10000 },
'1만원 ~ 3만원': { min: 10000, max: 30000 },
'3만원 ~ 5만원': { min: 30000, max: 50000 },
'5만원 ~ 7만원': { min: 50000, max: 70000 },
'7만원 ~ 10만원': { min: 70000, max: 100000 },
'10만원 이상': { min: 100000, max: Infinity },
'20만원 이상': { min: 200000, max: Infinity },

 

API에서 요구하는 Query String 형식과 현재 우리 프로젝트에서 처리하고 있는 필터 버튼 값이 상이했기에 이를 먼저 처리해 주는 작업을 진행했다. 가격과 배송료의 경우 상수 데이터로 각 옵션의 범위를 지정해주고 있었는데, 이 부분은 클릭한 버튼의 값을 받아와 min, max key에 각각 알맞은 value값을 넣어주는 함수를 만들어 처리했다.

// getFilterRangeFromKeyword 함수
 if (filterName !== 'all' && filterName !== '전체') {
 	// 카테고리 처리
    } else if (queryKey === 'price') {
      const priceRange = PRICE_BOUNDARIES[filterName];
      filterQuery = {
        ...filterQuery,
        priceQuery: {
          minPrice: priceRange.min,
          maxPrice: priceRange.max,
        },
      };
      return filterQuery;
    } else if (queryKey === 'shippingFee') {
      const shippingFeeRange = SHIPPING_FEE_BOUNDARIES[filterName];
      filterQuery = {
        ...filterQuery,
        shippingFeeQuery: {
          minShippingFees: shippingFeeRange.min,
          maxShippingFees: shippingFeeRange.max,
        },
      };
      return filterQuery;
    }
  } else {
    // 전체 버튼 눌렀을 경우
    return (filterQuery = {
      ...filterQuery,
      allQuery: {
        queryKey,
        filterName,
      },
    });
  }
}

 

2️⃣ 필터 쿼리 구현하기

여기서 엄청 헤맸었는데, 계속 구글링 해보고 비슷한 방식의 코드들을 확인해 보면서 Query String을 이용해서 처리를 해야 한다는 사실을 알게 되고, useSearchParams의 존재를 알게 되면서 유레카를 외쳤다.

 

getFilterRangeFromKeyword 함수의 반환 값이 필요한 이유는 현재 페이지의 URL 파라미터에 있는 값과 비교하여 사용자의 행동에 따라 실시간으로 Query String을 조합해 주기 위함이었다. 

useSearchParams를 활용해서 현재 페이지의 Query String 값을 확인할 수 있다. Query String 조합을 위해 커스텀 훅을 만들어 로직을 처리해 주기로 결정했다.

// useQueryParams 커스텀 훅
export const useQueryParams = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  
  // sortQueryParams 처리 로직
  
  // filterQueryParams 처리 로직
  const filterQueryParams = (queryKey: string, filterName: string) => {
    const filterQueryKeyword =
      getFilterRangeFromKeyword(queryKey, filterName) || {};

    const getAllSearchParams = Array.from(searchParams.entries()).filter(
      ([key]) => key !== 'sort',
    );

    const paramsKey = getAllSearchParams.map(([key, _]) => key);
    const paramsValue = getAllSearchParams.map(([_, value]) => value);

    const clickedFilterBtn = filterQueryKeyword?.allQuery?.filterName;

    if (clickedFilterBtn !== 'all' && clickedFilterBtn !== '전체') {
      // 조건 처리를 위해 key, value 추출
      const filterValue = Object.values(filterQueryKeyword)[0];

      // 현재 페이지에 쿼리 스트링이 존재하지 않는 경우 제일 처음 누른 필터에 대한 쿼리 추가
      if (!paramsKey.length && !paramsValue.length) {
        // 필터값에 따른 쿼리 처리
        Object.entries(filterValue).forEach(([key, value]) => {
          searchParams.set(`${key}`, `${value}`);
        });
        setSearchParams(searchParams);
      }
      // 쿼리 스트링이 존재하는 경우
      // 현재 페이지에 있는 쿼리 스트링의 key와 value가 클릭한 필터 값과 동일한지 확인
      const keysMatch = Object.keys(filterValue).every((key) =>
        paramsKey.includes(key),
      );
      const valuesMatch = Object.values(filterValue)
        .map((value) => String(value))
        .every((value) => paramsValue.includes(value));

      const isExistInFilterQuery = keysMatch && valuesMatch;

      // 동일한 값이 없을 경우 새로운 값으로 변경
      if (!isExistInFilterQuery) {
        Object.entries(filterValue).forEach(([key, value]) => {
          searchParams.set(`${key}`, `${value}`);
        });
        setSearchParams(searchParams);
      }
    } else {
      if (filterQueryKeyword?.allQuery?.queryKey === 'category') {
        searchParams.delete('category');
        setSearchParams(searchParams);
      } else if (filterQueryKeyword?.allQuery?.queryKey === 'price') {
        searchParams.delete('minPrice');
        searchParams.delete('maxPrice');
        setSearchParams(searchParams);
      } else if (filterQueryKeyword?.allQuery?.queryKey === 'shippingFee') {
        searchParams.delete('minShippingFees');
        searchParams.delete('maxShippingFees');
        setSearchParams(searchParams);
      }
    }
  };

  return [sortQueryParams, filterQueryParams];
};

 

코드를 설명하자면,

  • useSearchParams의 옵션인 entries()를 사용하여 현재 URL의 Query String의 key, value 값을 배열로 받아온다. (이때 sort가 key인 경우는 제외한다.)
  • 그리고 현재 사용자가 클릭한 버튼 값과(getFilterRangeFromKeyword 함수의 반환 값) 현재 URL의 Query String 값을 비교하고자 key와 value를 각각 배열에 담아준다.
    • 만약 동일한 값이 없다면, 새로운 값으로 쿼리값을 갱신해 준다.
    • 만약 해당 옵션에 대한 값이 아예 없다면, 새로운 값으로 추가해준다.
    • 전체 버튼을 클릭했다면 현재 URL 파라미터에 해당 옵션 key값이 있으면 해당 값을 삭제한다.

이런 식으로 API에서 요구하는 쿼리 형식을 변경해 줄 수 있었다.

 

3️⃣ API 요청 쿼리 구현하기

마지막으로 현재 페이지 URL에 구현한 Query String이 실제 API 요청에 사용될 수 있도록 요청 API 양식에 맞춰 처리를 해줘야 한다. 이 과정은 useSortFilter 커스텀훅을 만들어 처리해 줬다. (코드가 길어서 짧은 설명으로 대체한다.)

const location = useLocation();
const queryParams = location.search;
const [searchParams, setSearchParams] = useSearchParams(queryParams);

 

최종으로 완성된 URL의 Query String을 가져와 처리해줘야 했기에 useLocation의 search 속성을 통해 현재 페이지의 URL의 ? 뒤의 정보를 받아왔다. 이 정보를 useSearchParams에 초기값으로 넣어주고, 각 Query의 key값을 통해 value를 받아온다.

이때, 반드시 searchParams가 location 값이 변경되거나 새로고침 시에 변경 사항을 바로 확인하고 반영할 수 있도록 useEffect를 통해 의존성을 걸어주는 것이 중요하다.

 

그 다음 완성된 정렬 쿼리와 필터 쿼리를 조합해서 API 요청을 해주면 완성!

 

💡 결과 화면

 


참고

[공식문서] useSearchParams - React Router(링크)

[mdn] searchParams property - 링크

[mdn] URLSearchParams - 링크  


💬 본 포스팅은 공부 기록용으로 정확하지 않은 정보가 존재할 수 있습니다. 발견하신다면 알려주세요!