입점사 프로젝트를 진행하며 페이지네이션과 함께 많이 쓰인 것은 필터일 것이다!
각 페이지에서 데이터를 조회하기 위해 필터와 페이지네이션이 사용되었다.
각각의 페이지마다 필터 컴포넌트로 만들면서 그에 대한 로직이 반복되어 이를 개선하고 뒤로가기 또는 새로고침 시 필터값을 유지하는 방법을 알아보겠다!
필터 컴포넌트와 필터 관련 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
'IT > React' 카테고리의 다른 글
useState 원리 (+closure) 그리고 useCallback 언제 사용하면 좋을까? (0) | 2024.01.12 |
---|---|
recoil로 모달 전역 상태관리하기(+typescript) (1) | 2024.01.10 |
Pagination custom hook (0) | 2023.11.06 |
[React] Button Component 잘 만드는 방법! (0) | 2023.11.05 |
Pagination 구현하기 (0) | 2023.10.17 |