[Next.js] 데이터에서 검색어로 찾는 리스트 추출하기 (feat. hook으로 만들어 사용하기)

해달·2023년 5월 11일
0

개요

만약 받아온 알파벳순으로 정렬되어있는 리스트에서 필요한값을 찾는데 z로 시작되는 값이 필요하다면 유저는 제일 아래까지 탐색해서 원하는 값을 찾아야 할것이다.

리스트에서 원하는 값을 빠르게 찾을 수 있도록 검색기능을 추가해 필터의 사용성을 높여보았다.


구현사항

  1. 서버에서 받아온 배열데이터에서 검색어와 일치하는 글자가 있는 값만 보여준다.
    • 서버로 네트워크 요청을 보내지 않고 받아온 데이터로 클라이언트에서 처리하기
  2. 검색어와 일치하는 부분을 진하게 보여준다
    • 훅으로 처리해주기

실제 구현

1. 리스트 보여주기

const { data: brands } = useBrands();

서버에서 브랜드리스트를 받아와서 캐싱해놓는 과정을 진행한다 (react-query사용)
데이터를 캐싱해놓는다면 검색할때마다 서버로 데이터 요청을 보내지않고 클라이언트에서 받아온 데이터에서 필터링해서 보여줄 수 있다.
캐싱된 데이터를 사용한다면 서버로 불필요한 데이터를 줄일 수 있어 유저에게 보여주는 시간도 빨라진다.

useBrandSearch

브랜드리스트에서 검색한 브랜드리스트를 리턴해주는 훅을 만들어 사용한다.
위 훅에 사용되는 로직은 두군데 컴포넌트에서 사용될 예정이여서 중복로직을 작성하기 싫어 훅으로 빼내었다.

const useBrandSearch = () => {
  const { data: brands } = useBrands();
  const [searchTerm, setSearchTerm] = useState('');
  const [filteredBrands, setFilteredBrands] = useState<AllBrandsResponse[]>([]);
  • 브랜드리스트를 받아온다.
  • (1)검색어state (2)필터된 브랜드리스트 state를 만든다.
  useEffect(() => {
    if (brands) {
      const filteredList = brands.filter((brand) => {
        return brand.nameEn.toLowerCase().includes(searchTerm.toLowerCase());
      });

      setFilteredBrands(filteredList);
    }
  }, [searchTerm, brands]);
  • 브랜드 이름에서 검색어와 비교해 일치하는 값만 filter한다. (
    • 대소문자가 다를경우를 대비해 toLowerCase 사용
  const renderSearchInput = () => {
    return (
      <InputWrapper>
        <SearchInput
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
      </InputWrapper>
    );
  };
  • 동일한 형상의 Input컴포넌트를 사용할거기에 input component를 렌더링하는 함수도 만들어준다.
  return {
    searchTerm,
    filteredBrands,
    renderSearchInput,
  };
};

export default useBrandSearch;
  • 위에서 만들어놓은것중 필요한 값들은 return시켜 외부컴포넌트에서 사용할 수 있도록 한다.
    검색어, 필터된브랜드리스트, 검색어인풋창렌더링함수
  const { filteredBrands, renderSearchInput, searchTerm } = useBrandSearch();
  • 위에 훅에서 받아온 데이터로 필요한 곳에서 받아와서 사용해주면 된다.

추가적인 구현사항

리스트에서 유저가 브랜드를 클릭했을 때 만약 필터를 취소하고싶을때 아래와 같이 상단에 선택된 브랜드를 표기해주지 않는다면 유저는 1)다시 검색하거나 2)본인이 선택한 브랜드를 찾으러 리스트를 내리거나
둘 중의 하나의 행동을 해야한다.
사용자 입장에서는 매우 번거로울 수 있는 일이기에 선택 된 브랜드를 상단에 표기해주고 삭제할 수 있도록 해주어 편리성을 증가시켜주었다.


2. 검색어 하이라이팅 시키기

검색어를 입력했을 때 검색어와 일치하는 부분을 하이라이팅 시켜준다.

검색어와 name을 인자로 전달해 매칭되는 문자열의 스타일을 변경해주는 훅을 사용한다.

useMatchTerm

const useMatchTerm = (searchTerm: string, text: string) => {
  const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const regex = new RegExp(`(${escapedSearchTerm})`, 'gi');

  const splitName = text.split(regex);

  const renderHighlightMatchingText = () => {
    return splitName.map((part, index) => {
      const isMatch = part.toLowerCase() === searchTerm.toLowerCase();

      return isMatch ? <b key={index}>{part}</b> : part;
    });
  };

  return { renderHighlightMatchingText };
};

export default useMatchTerm;

escapedSearchTerm
검색어에서 특수문자들을 정규식 패턴으로 인식되지 않고 문자열로 인식될 수 있게 끔 이스케이프 처리를 해준 검색어로 만든다.

  • 정규표현식 [.*+?^${}()|[\]\\]
    대괄호([]) 안에 있는 문자들은 이스케이프되지 않아도 특별한 의미를 가지지 않고 그대로 일치하는 문자열로 해석된다. 따라서 여기서는 *, ., +, ?, ^, $, {}, (), |, [], \\ 문자들이 해당된다.

regex
new RegExp()를 사용하여 escapedSearchTerm을 포함하는 패턴을 생성

  • 첫번째로 전달 된 인자 : 이스케이프 처리 된 문자열

  • 두번째 매개변수 : gi 정규식 플래그

    • g : 전역검색
    • i : 대소문자 구분 없는 검색
    • 대소문자를 구분하지 않고 문자열 전체에서 모든 일치하는 부분을 찾는다
  • 여기서 소괄호로 정규표현식을 묶은건 일치하는 패턴을 하나의 그룹으로 묶어주는 역할을 하기 때문이다.

    • escapedSearchTerm에서 소괄호로 쌓인 부분은 그룹으로 처리
    • 변수에 저장된 이스케이프 처리된 검색어 패턴을 그룹으로 묶어서 일치하는 부분을 추출 할 수 있음
    • 한마디로 이스케이프처리된 검색어

splitName

  • split() 메서드는 정규식 패턴을 인자로 받아 해당 패턴을 기준으로 문자열을 분할한다.
  • text 문자열을 regex 객체에 지정된 패턴을 기준으로 분할한 결과를 담은 배열이다.
  • 여기서는 문자열이 제거되지 않고 정규식에 일치하는 부분을 기준으로 분리가 된다.
  • 정규식에서 소괄호는 그룹화를 위해 사용된다
    • 그러나 그룹화를 사용하지 않고 스플릿을 수행하면 그룹화된 부분은 결과에 포함되지 않고 제거됩니다. 그룹화된 부분은 패턴 일치 여부를 확인하거나 추출할 때 사용되는 목적으로 소괄호로 감싸게된다.

예시

const text = 'this is test sentence';
const searchTerm = 'is';
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regexWithParentheses = new RegExp(`(${escapedSearchTerm})`, 'gi');
const regexWithoutParentheses = new RegExp(`${escapedSearchTerm}`, 'gi');

const splitNameWithParentheses = text.split(regexWithParentheses);
const splitNameWithoutParentheses = text.split(regexWithoutParentheses);

console.log('With Parentheses:', splitNameWithParentheses);
console.log('Without Parentheses:', splitNameWithoutParentheses);

// With Parentheses: [ 'th', 'is', ' is test sentence' ]
// Without Parentheses: [ 'th', 'is', ' ', ' is test sentence' ]
  • is를 기준으로 문자열이 분리되는데, 소괄호가 있을 경우 is 자체도 분리된 결과에 포함됩니다. 소괄호로 감싸면 일치하는 패턴 자체도 결과에 포함되기 때문입니다. 소괄호가 없을 경우 is는 분리된 결과에 포함되지 않고 공백으로 된다.

renderHighlightMatchingText
입력된 검색어기준으로 분리된 배열을 map으로 돌면서 매치된다면 <b>태그로 스타일을 변경해서 렌더링해준다.

  const { renderHighlightMatchingText } = useMatchTerm(
    searchTerm,
    brand.nameEn
  );


<div>{renderHighlightMatchingText()}</div>

마치며

이 기능을 작업하면서 초반에는 훅으로 작성하지않고 기능이 구현되게끔 작업을 마친 뒤 공통으로 추출할 수 있는 부분을 확인하여 커스텀훅을 2개를 만들게 되었다.
훅으로 추출해내어 적용하게되니 어떤동작을하는지와 그에 관련된 상세구현은 따로 확인할 수 있어 수정도 편하고 가독성도 높아지니 훅을 더 잘써야겠따는 생각이 들었다 👀
금방 끝낼 기능이라고 생각했는데 하다보니 생각보다 신경 쓸 부분이 많았따

0개의 댓글