IT/React

필터 구현 로직 custom hook으로 만들기(+뒤로가기 시 필터값 유지)

솔B 2023. 12. 5. 22:32

입점사 프로젝트를 진행하며 페이지네이션과 함께 많이 쓰인 것은 필터일 것이다!

각 페이지에서 데이터를 조회하기 위해 필터와 페이지네이션이 사용되었다.

각각의 페이지마다 필터 컴포넌트로 만들면서 그에 대한 로직이 반복되어 이를 개선하고 뒤로가기 또는 새로고침 시 필터값을 유지하는 방법을 알아보겠다!

필터 컴포넌트와 필터 관련 hook 구현하기

필터 컴포넌트

초기 필터값들과 검색, 초기화 시 동작되는 함수를 전달 받는다.

사용자가 입력한 필터값을 내부적으로 관리한다.

 

src/components/filter/deliveryTemplateFilter.tsx

interface DeliveryTemplateFilterProps {
  initialFilterValues: FormState
  onSubmit: (filterValues: FormState) => void
  onReset: () => void
}

const DeliveryTemplateFilter = ({
  initialFilterValues,
  onSubmit,
  onReset,
}: DeliveryTemplateFilterProps) => {
  const [filterValues, setFilterValues] = useState(initialFilterValues)
  const { kind, type, searchType, search } = filterValues

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target as HTMLInputElement
    setFilterValues({
      ...filterValues,
      [name]: value,
    })
  }

  const handleSelect = (selectedOption: string | number, name: string) => {
    if (!name) return
    setFilterValues({
      ...filterValues,
      [name]: selectedOption,
    })
  }

  useEffect(() => {
    setFilterValues(initialFilterValues)
  }, [initialFilterValues])

  return (
    <>
      <FilterBox title="배송정보 관리">
        <GridForm>
          <GridRow title="배송유형" gap="12px">
            <RadioButton
              id="kindBtn"
              name="kind"
              value=""
              isChecked={kind === ''}
              onChange={handleChange}
            >
              전체
            </RadioButton>
            {KIND_LIST.map((kindOption, index) => (
              <RadioButton
                key={index}
                id={`kindBtn${kindOption.value}`}
                name="kind"
                value={kindOption.value}
                isChecked={kind === kindOption.value}
                onChange={handleChange}
              >
                {kindOption.name}
              </RadioButton>
            ))}
          </GridRow>
          <GridRow title="배송비종류" gap="12px">
            <RadioButton
              id="typeBtn"
              name="type"
              value=""
              isChecked={type === ''}
              onChange={handleChange}
            >
              전체
            </RadioButton>
            {TYPE_LIST.map((typeOption, index) => (
              <RadioButton
                key={index}
                id={`typeBtn${typeOption.value}`}
                name="type"
                value={typeOption.value}
                isChecked={type === typeOption.value}
                onChange={handleChange}
              >
                {typeOption.name}
              </RadioButton>
            ))}
          </GridRow>
          <GridRow title="검색" gap="16px">
            <Dropdown
              name="searchType"
              value={searchType}
              optionList={SEARCHTYPE_LIST}
              placeholder="검색 선택"
              onSelect={handleSelect}
            />
            <InputComponent
              type="text"
              name="search"
              value={search}
              ph="검색어를 입력하세요."
              onChange={handleChange}
            />
          </GridRow>
        </GridForm>
      </FilterBox>
      <FilterHandleBtnList onSearch={() => onSubmit(filterValues)} onReset={onReset} />
    </>
  )
}

export default DeliveryTemplateFilter

 

 

DeliveryTemplateFilter(필터 컴포넌트)를 페이지에 적용시켜보면 아래와 같다.

필터링 값이 변경되면 쿼리가 재실행된다.

src/pages/item/delivery-template.tsx

const DeliveryTemplatePage = () => {
  const queryClient = useQueryClient()

  const { filterForm, onSubmitFilter, onResetFilter } = useFilterForm<FormState>({
    kind: '',
    type: '',
    searchType: '템플릿코드',
    search: '',
  })
  const { kind, type, searchType, search } = filterForm
  const { pageInfo, setPageInfo, onChangePage, onChangePerPage } = usePageInfo()
  const { totalCount, currentPage, lastPage, perPage } = pageInfo
  
  const handleSubmitFilter = (values: FormState) => {
    setPageInfo({ ...pageInfo, currentPage: 1 })
    onSubmitFilter(values)
  }

  const handleResetFilter = () => {
  	setPageInfo({ ...pageInfo, currentPage: 1 })
    onResetFilter()
  }

  // 배송템플릿 list
  const {
    isLoading: isLoadingList,
    data: dataList,
    isFetching: isFetchingList,
  } = useQuery(
    ['deliveryTemplateList', filterForm, currentPage, perPage],
    () => getTemplateList(filterForm)
  )

  return (
    <DashboardLayout>
      <DashboardContainer>
        <DeliveryTemplateFilter
          initialFilterValues={filterForm}
          onSubmit={handleSubmitFilter}
          onReset={handleResetFilter}
        />
      </DashboardContainer>
		/* 필터에 대한 리스트 */
    </DashboardLayout>
  )
}

 

필터 관련 hook

src/hooks/useFilterForm.ts

import { useState } from 'react'

export default function useFilterForm<T>(initialForm: T) {
  const [filterForm, setFilterForm] = useState<T>(initialForm)

  const onSubmitFilter = (values: T) => {
    setFilterForm(values)
  }

  const onResetFilter = () => {
    setFilterForm(initialForm)
  }

  return { filterForm, onSubmitFilter, onResetFilter }
}

 

'뒤로가기' 시 이전의 필터값 유지하기

페이지를 새로고침하거나 뒤로가기로 이동 시 필터값이 유지되지 않고 사라지고 있었다.

이를 해결하기 위해서 필터값을 url의 query string에 저장하면 된다.

 

마켓컬리를 예시로 보면,

선택한 검색필터의 값과 page(현재 몇 페이지인지)등을 url에 저장하고 있다.

이렇게 하면 필터값 유지와 뒤로가기, 앞으로가기 뿐만 아니라 다른 사람에게 공유할 때도 필터값을 유지할 수 있다!

 

필터가 검색될 때마다 query에 해당 필터 값을 push 해서 url을 변경해 준다.

window 객체의 popstate 이벤트가 발생할 때마다 url의 query string에 있는 파라미터를 가져와서 초기 필터 값에 넣어준다.

 

여기서 query string을 다루기 위해 관련 라이브러리를 사용해도 좋지만

나는 간단하게 함수를 만들어서 사용했다.

 

아래와 같이 추가해 주면 잘 작동한다!

 

src/pages/item/delivery-template.tsx

const DeliveryTemplatePage = () => {
  const { query } = useRouter()
  const queryClient = useQueryClient()

  // 초기에 query의 값이 있다면 넣어주기 
  const { filterForm, onSubmitFilter, onResetFilter } = useFilterForm<FormState>({
    kind: query.kind ? String(query.kind) : '',
    type: query.type ? String(query.type) : '',
    searchType: query.searchType ? String(query.searchType) : '템플릿코드',
    search: query.search ? String(query.search) : '',
  }) 
  const { kind, type, searchType, search } = filterForm
  
  const { pageInfo, setPageInfo, onChangePage, onChangePerPage } = usePageInfo()
  const { totalCount, currentPage, lastPage, perPage } = pageInfo
  
  const handleSubmitFilter = (values: FormState) => {
    setPageInfo({ ...pageInfo, currentPage: 1 })
    onSubmitFilter(values)
  }

  const handleResetFilter = () => {
    setPageInfo({ ...pageInfo, currentPage: 1 })
    onResetFilter({
      kind: '',
      type: '',
      searchType: '템플릿명',
      search: '',
    })
  }

  // 배송템플릿 list
  const {
    isLoading: isLoadingList,
    data: dataList,
    isFetching: isFetchingList,
  } = useQuery(
    ['deliveryTemplateList', filterForm, currentPage, perPage],
    () => getTemplateList(filterForm)
  )

  return (
    <DashboardLayout>
      <DashboardContainer>
        <DeliveryTemplateFilter
          initialFilterValues={filterForm}
          onSubmit={handleSubmitFilter}
          onReset={handleResetFilter}
        />
      </DashboardContainer>
		/* 필터에 대한 리스트 */
    </DashboardLayout>
  )
}

 

src/hooks/useFilterForm.ts

import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { getQueryFilterForm, stringifyQuery, getParam } from 'src/utils/query'
import { PER_PAGE_LIST } from 'src/components/filter/PerPageDropdown'

export default function useFilterForm<T>(initialForm: T) {
  const { push, query } = useRouter()
  const [filterForm, setFilterForm] = useState<T>(initialForm)

  useEffect(() => {
    const handlePopState = () => {
      setFilterForm({
        ...initialForm,
        ...getQueryFilterForm(window.location.search),
      })
    }
    window.addEventListener('popstate', handlePopState)
    return () => window.removeEventListener('popstate', handlePopState)
  }, [])

  const onSubmitFilter = (values: T) => {
    setFilterForm(values)

    push({
      query: stringifyQuery({
        ...query,
        ...values,
        page: 1,
      }),
    })
  }

  const onResetFilter = (values: T) => {
    setFilterForm(values)

    const perPage = getParam('perPage')
    push({
      query: {
        perPage: perPage || PER_PAGE_LIST[0].value,
      },
    })
  }

  return { filterForm, onSubmitFilter, onResetFilter }
}

 

 

 

참고자료 - https://www.daleseo.com/react-filter/#google_vignette