URL 검색 매개변수를 활용하여 API에 보낼 여러 필터 데이터 관리하기

kiwon kim·2024년 10월 3일

Frontend

목록 보기
7/30
post-thumbnail

현대 웹 애플리케이션에서는 사용자 입력에 따라 다양한 필터링 옵션을 제공하고, 이를 기반으로 API에서 데이터를 가져오는 일이 빈번합니다. 이때 필터 상태를 효과적으로 관리하고, URL과 동기화하여 사용자 경험을 향상시키는 방법 중 하나가 바로 URL 검색 매개변수(search parameters)를 사용하는 것입니다.

이번 포스트에서는 useQueryParamsState라는 커스텀 훅을 만들어, 여러 필터 데이터를 URL 검색 매개변수로 관리하고 API에 전달하는 방법을 소개하겠습니다.

왜 URL 검색 매개변수를 사용할까요?

  • 상태 유지: 사용자가 페이지를 새로고침하거나 다른 페이지로 이동했다 돌아와도 이전에 설정한 필터 상태를 유지할 수 있습니다.
  • 공유 용이성: 필터링된 결과를 다른 사람과 공유할 때 URL만 전달하면 동일한 상태를 재현할 수 있습니다.
  • 북마크 가능: 사용자가 특정 필터 조합을 즐겨찾기 할 수 있습니다.
  • SEO 개선: 검색 엔진이 다양한 상태의 페이지를 크롤링하여 색인화할 수 있습니다.

useQueryParamsState 커스텀 훅 만들기

먼저, URL 검색 매개변수와 상태를 동기화하는 커스텀 훅을 만들어보겠습니다.

코드 구현

import { useState, useEffect, Dispatch, SetStateAction } from "react";
import { useLocation } from "react-router-dom";

type UseQueryParamsStateReturnType<T> = [T, Dispatch<SetStateAction<T>>];

interface UseQueryParamsStateOptions {
  shouldClearParamsOnRefresh?: boolean;
}

export const useQueryParamsState = <T extends object>(
  initialState: T,
  options?: UseQueryParamsStateOptions
): UseQueryParamsStateReturnType<T> => {
  const location = useLocation();

  // 객체 상태로 여러 쿼리 파라미터를 관리
  const [value, setValue] = useState<T>(() => {
    if (typeof window === "undefined") return initialState;

    const { search } = window.location;
    const searchParams = new URLSearchParams(search);
    const paramsObject: Partial<T> = {};

    // URL에서 쿼리 파라미터를 읽어서 객체로 변환
    searchParams.forEach((paramValue, paramKey) => {
      paramsObject[paramKey as keyof T] = paramValue as T[keyof T];
    });

    return { ...initialState, ...paramsObject } as T;
  });

  // 쿼리 파라미터를 URL에 반영하는 효과
  useEffect(() => {
    const currentSearchParams = new URLSearchParams(window.location.search);

    Object.keys(value).forEach((key) => {
      const paramValue = value[key as keyof T] as T[keyof T];
      if (paramValue !== null && paramValue !== "") {
        currentSearchParams.set(key, String(paramValue));
      } else {
        currentSearchParams.delete(key);
      }
    });

    // 새로운 URL 조합
    const newUrl = [window.location.pathname, currentSearchParams.toString()]
      .filter(Boolean)
      .join("?");

    window.history.replaceState(window.history.state, "", newUrl);
  }, [value, location.pathname]);

  // 새로고침 시 쿼리 파라미터 제거 옵션 처리
  useEffect(() => {
    const shouldClearParams = options?.shouldClearParamsOnRefresh ?? false;

    // 컴포넌트가 처음 마운트될 때만 쿼리 파라미터 제거
    if (shouldClearParams) {
      const clearSearchParams = () => {
        setValue(initialState); // 초기 상태로 되돌림
        const newUrl = window.location.pathname;
        window.history.replaceState(null, "", newUrl);
      };

      // 새로고침 등 첫 마운트 시에만 쿼리 파라미터 제거
      clearSearchParams();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [options?.shouldClearParamsOnRefresh]);

  return [value, setValue];
};

구현 설명

  • 상태 초기화: useState 훅을 사용하여 URL의 검색 매개변수를 객체 형태로 상태에 저장합니다. 이때 initialState와 URL에서 가져온 매개변수를 병합하여 초기 상태를 설정합니다.
  • URL 동기화: useEffect 훅을 통해 상태(value)가 변경될 때마다 URL의 검색 매개변수를 업데이트합니다. 이로써 상태와 URL이 항상 동기화됩니다.
  • 옵션 처리: shouldClearParamsOnRefresh 옵션을 통해 컴포넌트가 처음 마운트될 때 검색 매개변수를 제거할지 여부를 결정합니다.

컴포넌트에서의 활용 예시

이제 이 커스텀 훅을 사용하여 사용자 입력에 따라 필터링된 데이터를 API에서 가져오는 컴포넌트를 만들어보겠습니다.

코드 구현

import { useQuery } from "@tanstack/react-query";
import { api } from "./api";
import { useQueryParamsState } from "./useQueryParamsState";
import { AxiosResponse } from "axios";
import { debounce } from "lodash";
import { useMemo } from "react";

const Comp1 = () => {
  const [searchParams, setSearchParams] = useQueryParamsState(
    {
      test1: "",
      test2: "",
      isSearching: false,
    },
    {
      shouldClearParamsOnRefresh: false,
    }
  );

  const enabled =
    typeof searchParams.isSearching === "string"
      ? searchParams.isSearching === "false"
        ? true
        : false
      : !searchParams.isSearching;

  const a = useQuery<
    AxiosResponse<{ results: { name: string; id: string }[] }>
  >({
    queryKey: ["GET_MOVIE", searchParams],
    queryFn: async () => {
      const response = await api.get(
        `/3/search/keyword?query=${searchParams.test1}&page=1`
      );

      return response;
    },
    enabled,
  });

  const handleSearchingStatus = useMemo(
    () =>
      debounce(
        () =>
          setSearchParams((prev) => {
            return {
              ...prev,
              isSearching: false,
            };
          }),
        3000
      ),
    [setSearchParams]
  );

  return (
    <div>
      <input
        name="test1"
        value={searchParams.test1}
        onChange={(e) => {
          handleSearchingStatus();
          setSearchParams((prev) => {
            return {
              ...prev,
              isSearching: true,
              [e.target.name]: e.target.value,
            };
          });
        }}
      />
      <input
        name="test2"
        value={searchParams.test2}
        onChange={(e) => {
          setSearchParams((prev) => {
            return {
              ...prev,
              isSearching: true,
              [e.target.name]: e.target.value,
            };
          });
        }}
      />
      {a.data?.data.results.map((el) => {
        return <div key={el.id}>{el.name}</div>;
      })}
    </div>
  );
};

export default Comp1;

구현 설명

  • 상태 관리: useQueryParamsState 훅을 사용하여 test1, test2, isSearching 세 가지 필터 상태를 관리합니다.
  • 쿼리 활성화 조건: enabled 변수를 통해 isSearching 상태에 따라 쿼리 실행 여부를 결정합니다. 검색 중일 때는 쿼리를 비활성화하여 불필요한 API 호출을 방지합니다.
  • API 호출: useQuery 훅을 사용하여 검색어에 따라 API에서 데이터를 가져옵니다. queryKeysearchParams를 포함하여 검색어가 변경될 때마다 새로운 데이터를 가져오도록 합니다.
  • 디바운싱: handleSearchingStatus 함수를 useMemodebounce를 사용하여 생성합니다. 사용자가 입력을 멈추고 3초 후에 isSearching 상태를 false로 변경하여 쿼리를 활성화합니다.
  • 입력 필드 처리:
    • 첫 번째 입력 필드(test1)에서는 사용자가 입력할 때마다 isSearchingtrue로 설정하고, 디바운싱된 함수로 isSearchingfalse로 변경합니다.
    • 두 번째 입력 필드(test2)에서는 isSearchingtrue로 설정하지만 디바운싱 없이 바로 상태를 업데이트합니다.

이 접근법의 장점

  • 효율적인 API 호출 관리: isSearching 상태와 enabled 옵션을 사용하여 불필요한 API 호출을 방지하고, 사용자가 입력을 마친 후에만 데이터를 가져옵니다.
  • 사용자 경험 향상: 디바운싱을 통해 입력 도중에 API 호출이 발생하지 않아 사용자 인터페이스의 응답성이 향상됩니다.
  • 상태와 URL의 동기화: 사용자의 필터 입력이 URL에 반영되어 새로고침이나 공유 시에도 동일한 상태를 유지합니다.
  • 코드 재사용성: 커스텀 훅을 사용하여 상태 관리 로직을 재사용 가능하게 만들어 코드의 유지 보수성을 높였습니다.

추가 개선 사항

  • 타입 안정성 강화: useQueryParamsState 훅에서 제네릭 타입 Tobject로 지정하여 더 다양한 형태의 상태를 지원할 수 있도록 했습니다.
  • 불필요한 매개변수 제거: 값이 null이거나 빈 문자열인 경우 URL에서 해당 매개변수를 제거하여 URL을 깨끗하게 유지합니다.
  • 커스터마이징 가능: 필요에 따라 훅의 옵션이나 상태 관리 로직을 변경하여 다양한 요구사항에 대응할 수 있습니다.

결론

URL 검색 매개변수를 활용하여 여러 필터 데이터를 관리하면 사용자 경험을 향상시키고 애플리케이션의 상태 관리도 용이해집니다. 특히, API 호출을 효율적으로 관리하고 상태와 URL을 동기화하여 사용자에게 일관된 경험을 제공할 수 있습니다.

useQueryParamsState와 같은 커스텀 훅을 사용하면 이러한 기능을 손쉽게 구현할 수 있으며, 코드의 재사용성과 유지 보수성도 높일 수 있습니다. 앞으로 복잡한 필터링 기능을 구현하거나 사용자 상태를 URL과 동기화해야 하는 경우 이와 같은 방법을 고려해보시기 바랍니다.


참고: 이 글의 예제 코드는 이해를 돕기 위한 간단한 버전입니다. 실제 애플리케이션에서는 에러 처리, 로딩 상태 관리, 입력 값 유효성 검사 등을 추가로 구현해야 합니다.

profile
FOR_THE_BEST_DEVELOPER

0개의 댓글