지난 번에 작성한 Tanstack Query를 사용해 무한스크롤을 구현한 글에서 이어집니다.
[PPLog] Tanstack Query + Intersection Observer를 사용해 무한 스크롤 구현하기!
사용자가 검색창에 검색어를 입력한 뒤, 검색 결과에 Tanstack Query를 사용해 무한스크롤로 보여줍니다.
useCustomInfiniteQuery를 사용해 data를 가져오는 부분은 이전과 같습니다.
이제 searchTerm과 searchValue 상태를 정의합니다.
const [searchValue, setSearchValue] = useState(''); // searchValue와 searchTerm을 구분해야
const [searchTerm, setSearchTerm] = useState(''); // onChange가 완료되고 검색 함수 실행 때만 api호출 할 수 있음
이렇게 searchTerm과 searchValue를 구분한 주된 이유는 API 호출 최적화 때문입니다.
이 둘은 다음과 같은 특징을 가집니다.
searchValue
- 입력창의 실시간 변화 감지
- 사용자가 타이핑할 때마다 즉시 변경, 입력 중인 상태 반영
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를 사용해서 다음과 같은 일을 수행합니다.
gi플래그 사용 : g(전역 검색), i(대소문자 구분 없음)
2,(${searchTerm})의 괄호는 겁색어를 그룹으로 유지하여 분할 결과에 포함
그리고 특수문자 처리와 텍스트 분할 과정은 다음과 같이 동작합니다:
escapedSearchTerm은 검색어에 포함된 특수문자들을 이스케이프 처리합니다:
/[.*+?^${}()|[\]\\]/g: 정규표현식에서 특별한 의미를 가지는 메타문자들을 매치\\$&: 매치된 특수문자 앞에 백슬래시를 추가하여 일반 문자로 처리text.split()으로 텍스트를 분할할 때:
["Hell", "o", " W", "o", "rld"]와 같이 분할됨이렇게 분할된 텍스트 조각들은 map 함수를 통해 순회하면서, 검색어와 일치하는 부분만 하이라이트 처리됩니다.
그 다음 대소문자 구분 없이 text를 비교하기 위해 context.toLowerCase() === searchTerm.toLowerCase()를 작성합니다.
이 함수를 통해 검색어와 일치하는 부분의 텍스트를 #F6C000색으로 표시해줄 수 있습니다.