[PPLOG] Tanstack-query를 사용해 검색 기능과 무한스크롤 처리하기

김현중·2024년 11월 30일

PPLOG

목록 보기
10/14
post-thumbnail

지난 번에 작성한 Tanstack Query를 사용해 무한스크롤을 구현한 글에서 이어집니다.
[PPLog] Tanstack Query + Intersection Observer를 사용해 무한 스크롤 구현하기!


사용자가 검색창에 검색어를 입력한 뒤, 검색 결과에 Tanstack Query를 사용해 무한스크롤로 보여줍니다.

useCustomInfiniteQuery를 사용해 data를 가져오는 부분은 이전과 같습니다.

이제 searchTermsearchValue 상태를 정의합니다.

  const [searchValue, setSearchValue] = useState(''); // searchValue와 searchTerm을 구분해야
  const [searchTerm, setSearchTerm] = useState(''); // onChange가 완료되고 검색 함수 실행 때만 api호출 할 수 있음

이렇게 searchTermsearchValue를 구분한 주된 이유는 API 호출 최적화 때문입니다.
이 둘은 다음과 같은 특징을 가집니다.

  1. searchValue
    • 입력창의 실시간 변화 감지
    • 사용자가 타이핑할 때마다 즉시 변경, 입력 중인 상태 반영
  2. searchTerm
    • 실제 검색 API를 호출할 때 사용되는 값
    • Enter키를 누르거나 검색 버튼을 클릭할 때만 업데이트됨
    • useCustomInfiniteQuery의 의존성 배열에 사용

그 다음 검색 함수와 인풋을 작성합니다.

  const handleSearch = (e?: { key: string }) => {
    if (searchValue === '') return;
    if (!e || e.key === 'Enter') {
      setSearchTerm(searchValue);
    }
  };

	...

   <input
      className="w-full focus:outline-none"
      placeholder="검색어를 입력해주세요"
      value={searchValue}
      onChange={(e) => setSearchValue(e.target.value)}
      onKeyDown={handleSearch}
	/>

이 함수의 매개변수 e는 선택적 매개변수로, 키보드 이벤트 객체를 받습니다. 그리고 검색 아이콘 클릭 시에는 이벤트 없이 호출이 됩니다.

그 다음 이 함수는 검색어가 비어있으면 함수를 종료하고, 사용자가 검색아이콘을 클릭하여 이벤트가 없을 때(!e), 혹은 Enter키를 눌렀을 때 검색을 실행하는 함수입니다.

그리고 인풋의 onKeyDown은 키보드 입력을 감지하여 Enter키 입력 시 검색을 실행하게 해줍니다.


✨검색어 하이라이트 처리하기

그 다음 사용자가 입력한 검색어와 포스트의 내용과 일치하는 부분을 하이라이트 처리하기 위한 함수를 작성합니다.

const highlightText = (text: string | JSX.Element): string | JSX.Element => {
    if (typeof text !== 'string') return text; //  string이 아닐 경우 함수 종료
  
    if (!searchTerm) return text;
  
     // 검색어의 특수문자 이스케이프 처리
    const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  
    // 전체 텍스트를 검색어 기준으로 분할, 'gi' 플래그: g(전역 검색), i(대소문자 구분 없음)
    const contexts = text.split(new RegExp(`(${searchTerm})`, 'gi'));
  
    // 검색어가 포함되지 않은 경우 원본 텍스트 반환
    if (contexts.length === 1) return text;
    return (
      <>
        {contexts.map((context) =>
          context.toLowerCase() === searchTerm.toLowerCase() ? (
            <span key={v4()} className="text-[#F6C000]">
              {context}
            </span>
          ) : (
            context
          ),
        )}
      </>
    );
  };

먼저 이 함수는 RegExp를 사용해서 다음과 같은 일을 수행합니다.

  1. gi플래그 사용 : g(전역 검색), i(대소문자 구분 없음)
    2, (${searchTerm})의 괄호는 겁색어를 그룹으로 유지하여 분할 결과에 포함

그리고 특수문자 처리와 텍스트 분할 과정은 다음과 같이 동작합니다:

  1. escapedSearchTerm은 검색어에 포함된 특수문자들을 이스케이프 처리합니다:

    • /[.*+?^${}()|[\]\\]/g: 정규표현식에서 특별한 의미를 가지는 메타문자들을 매치
    • \\$&: 매치된 특수문자 앞에 백슬래시를 추가하여 일반 문자로 처리
  2. text.split()으로 텍스트를 분할할 때:

    • 검색어를 기준으로 텍스트를 나누되, 검색어 자체도 결과 배열에 포함
    • 예를 들어, 텍스트가 "Hello World"이고 검색어가 "o"라면: 결과는 ["Hell", "o", " W", "o", "rld"]와 같이 분할됨

    이렇게 분할된 텍스트 조각들은 map 함수를 통해 순회하면서, 검색어와 일치하는 부분만 하이라이트 처리됩니다.


그 다음 대소문자 구분 없이 text를 비교하기 위해 context.toLowerCase() === searchTerm.toLowerCase()를 작성합니다.

이 함수를 통해 검색어와 일치하는 부분의 텍스트를 #F6C000색으로 표시해줄 수 있습니다.

profile
진짜 성실한 사람

0개의 댓글