사이드 프로젝트를 하는 도중 네이버처럼 검색 기능이 필요해서 다른 블로그를 찾아봤는데, api를 가져와서 구현하는 기능이 없길래 블로깅한다..(있다면 유감..👀)
(👆 이렇게 "구글"만 치면 여러개 쭉~ 기능이다.)
우선 최종 결과물 🍳
함수를 여러 번 호출하고 마지막 호출에서 일정 시간이 지난 후 해당 함수의 기능이 동작하는 기법
//custom hook으로 뺐는데, 한번만 쓸거라면 굳이 커스텀으로 안써도 된다.
import React, { useState, useEffect } from "react";
//value는 내가 호출하게 될 keyword고 delay는 말 그대로 delay다.
function useDebounce(value, delay) {
const [debounceValue, setDebounceValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebounceValue(value);
}, delay);
return () => {
//clearTimeout을 해주는 이유? 이전에 요청한 api가 남아있지 않도록 해줌
clearTimeout(handler);
};
}, [value, delay]);
return debounceValue;
}
export default useDebounce;
⚠️ react-hook-form이라는 라이브러리를 써서 코드가 생소할 수 있는데, 사실 별거 없다..
//Layout.js
const methods = useForm();
const [keyword, setKeyword] = useState([]);
const [count, setCount] = useState(0);
//methods.watch("search")은 e.target.value로 생각하면 된다.
const debouncedSearch = useDebounce(methods.watch("search"), 2000);
const searchListRef = useRef(null);
const [searchList, setSearchList] = useState(false);
useEffect(() => {
const getSearchRecipe = async () => {
const {
data: { totalCount, recipes },
} = await GET_SEARCH_RECIPE(1, count, debouncedSearch);
setCount(totalCount);
setKeyword(recipes);
};
//api 업데이트를 위해 의존성 배열에 debouncedSearch를 추가함
if (debouncedSearch) getSearchRecipe();
}, [debouncedSearch]);
useEffect(() => {
//click에 따라서 불리언 값을 줘서 list가 나오게 하려는 의도
//return을 한 이유는 컴포넌트가 사라질 때(false) 이벤트 동작을 할 필요 없으니 넣어줬다.
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}, []);
//ref를 사용해 어디를 찍고있는지 알려주었다. (나중에 나올 input 컴포넌트에서 다시 설명하겠음)
const handleClickOutside = (e) => {
if (searchListRef.current && !searchListRef.current.contains(e.target))
setSearchList(false);
};
//중요한 코드는 아니지만 대충 설명하자면 value가 없다면 Snackbar 라이브러리를 통해 알려주도록했다. 값이 들어온다면 페이지 이동을 하게한다.
const onSubmit = async () => {
if (methods.watch("search") === "") {
enqueueSnackbar("검색어를 입력해주세요", {
variant: "error",
});
} else if (methods.watch("search")) {
await router.push(`/search/${methods.watch("search")}`);
}
};
return(
//svg 때문에 코드가 지저분해서 컴포넌트로 빼고 onChange 값을 공유하기 위해 react-hook-form 라이브러리를 씀
<FormProvider {...methods}>
<SearchRecipeInput
onSubmit={methods.handleSubmit(onSubmit)}
keyword={keyword}
searchList={searchList}
setSearchList={setSearchList}
searchListRef={searchListRef}
/>
</FormProvider>
)
//SearchRecipeInput.js
//넘겨준 props을 받고, input의 값을 알기위해 react-hook-form 라이브러리에 register을 쓴 상황
const SearchRecipeInput = ({
onSubmit,
keyword,
searchList,
setSearchList,
searchListRef,
}) => {
const { register } = useFormContext({
mode: "onChange",
});
return (
//앗.. 쓰면서 발견한 쓸데없는 프레그먼트 (<></>) 지워도 상관없다.
<>
//앞서 언급한 ref가 쓰이는 이유는 form을 클릭하면 list가 보여야하기 때문에 넣어줬다.
<form
onSubmit={onSubmit}
className="mt-2 flex flex-col relative"
ref={searchListRef}
>
<div className="flex items-center">
<input
className="w-[350px] h-[30px] rounded px-2 text-xs relative"
type="text"
name="search"
placeholder="음식명, 재료명으로 검색해주세요."
//...register("search")는 onChage로 보면된다.
{...register("search")}
autoComplete="off"
//클릭하면 list가 나타나야하기 때문에 넣어둠
onClick={() => setSearchList(!searchList)}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 absolute right-2"
fill="none"
viewBox="0 0 24 24"
stroke="gray"
strokeWidth={2}
//돋보기 아이콘도 클릭하면 검색 페이지로 이동해야해서 넣어둠
onClick={onSubmit}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
//keyword는 여러개이니 배열로 받아서 map으로 보여주고있다. 일정 height를 지나면 스크롤로 보여주게 처리했다.(테일윈드 overflow-y-auto h-28 참고)
{searchList && (
<ul className="px-3 bg-white border-solid border border-slate-300 overflow-y-auto h-28 z-10">
{keyword.map((word) => (
<li
key={word.pk}
className="text-sm py-1 hover:fill-slate-600 hover:cursor-pointer"
//리스트를 클릭해도 결과 페이지로 이동해야하기 때문에 onClick을 넣어줬다.
onClick={() => {
router.push(`/search/${word.title}`);
}}
>
{word.title}
</li>
))}
</ul>
)}
</form>
</>
);
};
export default SearchRecipeInput;
이렇게해서 간....단하지 않고 눈물겨웠던 검색기능을 마치며... 회고록을 조만간 포스팅하겠다 😭
🌐 참고 사이트
https://developer-talk.tistory.com/248
https://youtu.be/PySFIsgXNZ0