이번 프로젝트에서 가장 어려웠던 부분을 꼽으라면 여기다.
검색필터 여러개 걸기.
와...
Virtual List에서 가장 오래 막혀있긴 했지만
그건 구현방법을 이해하는게 헷갈렸어서 그렇지 검색하면 도움이 되는 자료가 정말 많았다.
그런데 여러개의 검색조건을 거는 건 내 구글링 키워드가 잘못된건가?
구글링을 해도 자료를 찾기 쉽지 않아서 정말 막막했다.
우선 각 필터는 이렇게 만들었다.
날짜는 검색을 시작할 일자와 종료할 일자를 달력에서 선택할 수 있도록 했다. 여기에는 react-datepicker 라이브러리를 사용했는데 css 커스텀이 쉽지 않았다. (css 커스텀 스토리는 나중에 정리할 예정) 우측에 "초기화" 버튼을 누르면 선택했던 날짜가 초기화 된다.
결제 상태는 일반 button 태그를 사용했다.
다만 클릭이 되면 폰트와 보더의 색상이 바뀌도록 설정했다.
차량번호 input에는 Debounce를 사용했다.
직접 적용을 해보니 앞서 ResultList에 사용했던 Throttle과 어떻게 다른지 더 잘 이해가 되었다.
더 훌륭한 정의가 많지만 정말 간단하게 비교를 하자면
이렇게도 정리할 수 있지 않을까 싶다.
- Debounce는 딜레이의 기준을 마지막 액션에 주는 것
- Throttle은 딜레이의 기준을 최초의 액션에 주는 것
225ms의 딜레이를 주는 Debounce를 차량번호 Input에 걸어주어서
무의미한 검색결과 렌더링 횟수를 줄이고자 했다.
(사람이 인지 하지 못하는 자연스러운 딜레이 시간은 225ms라고 들었는데 구글링에 실패함..)
처음에 조건 하나를 걸때에는 괜찮았다.
useEffect와 .filter() 메소드를 사용했다.
useEffect(() => {
const plateNumKeywordFilteredList = filteredList.filter(
(item) =>
typeof plateNumKeyword !== "undefined" &&
item.plateNum.includes(plateNumKeyword)
);
setFilteredList(plateNumKeywordFilteredList);
}, [plateNumKeyword]);
그런데 조건이 2개 이상으로 늘어날때부터가 문제였다.
단순히 filter 내부에 조건을 "||" or 연산자로 이어주면 될거라고 생각한게 (완전) 오산이었다. 그렇게 하니 조건 둘중 하나만 맞아도 모두 검색이 되어버렸다.
그래서 각 필터조건별로 dependency를 가지는 useEffect를 따로 만들고
조건이 변경될때 마다 filteredList를 setState 하도록 해보았다.
(filteredList는 초기값으로 온전한 모든 데이터리스트를 가지고 있다.)
하지만 이것도 아니었다.
이렇게 하면 조건을 적용할때는 괜찮을지 몰라도
조건을 해제할때에는 filterdList의 state가 이미 조각나있기 때문에
검색결과가 정상적일 수 없었다.
여기서부터는 도저히 모르겠어서 주변 개발자분께 도움을 많이 받았다.
위에서 만난 문제들을 해결하려먼
1. 조건간의 교집합을 반환 한다.
2. 해당 조건이 없으면 무시하고 지나간다.
3. 원본이나 검색조건이 변경될때 마다 원본에 대한 필터링을
처음부터 다시 한다.
이걸 구현을 해야 했다.
여기서 filter에 체이닝을 할 수 있다는 걸 알았다.
조건A에 대한 .filter()를 하고 난 결과물에 조건B에 대한 .filter()를 하는 것이다.
하지만 이렇게 하면 filter 메소드를 실행할때마다
해당 시점의 대상이 되는 리스트 전체를 순회하는 걸 반복하는 양상이 되었다.
이걸 피하기 위해서 .reduce()를 사용했다.
.reduce()를 사용하면 데이터 하나에 모든 조건을 테스트하고 해당되는 값만 누적시킨다. 그러니까.. 매번 순회를 하는 filter와 달리 원샷원킬이랄까.
// 변경사항이 있을때마다 원본 list에 필터를 새로 적용한다.
const filterData = () => {
//아무 필터도 없는 맨 처음은 원본 list가 나와야 함
if (
payStatusKeyword &&
plateNumKeyword &&
startDate &&
endDate === undefined
) {
setFilteredList(list);
} else { // 실제 필터를 적용하는 부분
const filteredList = list.reduce<PayDataListProps[]>((acc, cur) => {
const payStatusKeywordCondition = payStatusKeyword
? cur.payStatus === payStatusKeyword
: true; // 해당 조건이 없으면 그냥 무시하고 지나간다.
const payNumKeywordCondition =
plateNumKeyword && plateNumKeyword.length > 0
? cur.plateNum.includes(plateNumKeyword)
: true;
const startDateCondition = startDate
? startDate.getTime() - new Date(cur.payDate).getTime() <= 0
: true;
const endDateCondition = endDate
? endDate.getTime() - new Date(cur.payDate).getTime() >= 0
: true;
// 해당 조건이 있다면 그에 부합하는 교집합인 놈만 push 하겠다.
if (
payStatusKeywordCondition &&
payNumKeywordCondition &&
startDateCondition &&
endDateCondition
) {
acc.push(cur);
}
return acc;
}, []);
setFilteredList(filteredList);
}
};
// 원본이 갱신되거나, 검색조건이 변경되면 filterData를 실행한다.
useEffect(() => {
filterData();
}, [list, payStatusKeyword, plateNumKeyword, startDate, endDate]);
이렇게 지지고 볶아서 결국 필터를 완성했다...
👉 ResultList.tsx 전체 보기
너무 애를 먹었던 부분이라 혹시 나와 같은 어려움을 겪고 있는 분이 계시다면 이 글이 조금이나마 도움이 되기를...
필터를 여러개 거는 것 만큼은 아니지만 또 한참 진땀을 뺐던 것이 "날짜값 비교" 였다.
분명 달력에서 선택한 날짜의 값이랑 리스트에 있는 날짜값이 동일한데
날짜 필터를 걸어도 검색결과가 나오지 않았기 때문이다.
결국엔 .getTime() 메소드를 사용하니 비교가 가능했다ㅠㅠ
.valueOf()도 결과적으로 완전 같은 값을 내놓는 동일한 메소드라고 한다.
1970년 1월 1일 00:00:00 UTC로부터의 경과시간을 밀리초 단위로 반환한다.
(* 참고: MDN의 [Date.prototype.valueOf()])
처음에는 Number(new Date(날짜string))
을 사용해서 비교했었는데
아마 밀리초까지의 모든 단위를 정확히 비교해주지 못했나보다...
(당시에는 Number()를 사용하면 비교대상이 서로 같은 값이 나왔기 때문에 도대체 왜애- 안되는지 정말 알수가 없었다ㅠㅠ)
const startDateCondition = startDate
? startDate.getTime() - new Date(cur.payDate).getTime() <= 0
: true;
const endDateCondition = endDate
? endDate.getTime() - new Date(cur.payDate).getTime() >= 0
: true;
그럼 이제 검색필터 정말 끝!
swimjiy님의 [JS map, filter, reduce 함수 톺아보기]
daesuni님의 [for, foreach, filter, map, reduce 기능 및 성능 비교]
MDN의 [Date]
DelftStack의 [JavaScript에서 두 날짜를 비교하는 방법]