No 15. 자동 검색 기능 구현(+debounce)

Jetom·2022년 5월 24일
0

react

목록 보기
15/15
post-thumbnail

사이드 프로젝트를 하는 도중 네이버처럼 검색 기능이 필요해서 다른 블로그를 찾아봤는데, api를 가져와서 구현하는 기능이 없길래 블로깅한다..(있다면 유감..👀)

(👆 이렇게 "구글"만 치면 여러개 쭉~ 기능이다.)

우선 최종 결과물 🍳


debounce란?

함수를 여러 번 호출하고 마지막 호출에서 일정 시간이 지난 후 해당 함수의 기능이 동작하는 기법

//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

profile
사람이 좋은 인간 리트리버 신혜리입니다🐶

0개의 댓글