개발새발 로그

[2024-02-12] 개인 프로젝트 회고 - 검색 기능 및 연관 검색어 추가 기능 구현 본문

TIL

[2024-02-12] 개인 프로젝트 회고 - 검색 기능 및 연관 검색어 추가 기능 구현

이즈흐 2024. 2. 12. 22:17

1. 연관 검색어를 모달처럼 닫을 수 있게 해 보자!

검색어를 입력해서 연관검색어가 나오게 되고, 만약 다른 요소(배경)를 클릭하면 연관검색어가 보이지 않는 기능을 만들었다.

먼저 코드를 작성한 코드를 보자!

import { useEffect, useRef, useState } from 'react'

const useModal = () => {
  const modalRef = useRef<HTMLUListElement | null>(null)
  const [isShow, setIsShow] = useState(false)

  const handleOutsideClick = (e: MouseEvent) => {
    const isOutsideClick =
      modalRef.current &&
      e.target instanceof Element &&
      !modalRef.current.contains(e.target)

    if (isOutsideClick) {
      setIsShow(false)
    }
  }

  useEffect(() => {
    if (isShow) {
      document.addEventListener('mousedown', handleOutsideClick)
    } else {
      document.removeEventListener('mousedown', handleOutsideClick)
    }

    return () => {
      document.removeEventListener('mousedown', handleOutsideClick)
    }
  }, [isShow])

  return { modalRef, isShow, setIsShow }
}
export default useModal

modal처럼 기능하고, 모듈화가 가능했기에 useModal이라는 커스텀 훅으로 만들었다.

내가 위에서 말했던 기능을 만들려면 4가지 조건이 있어야 했다.

1. 모달내부를 클릭했는지 외부를 클릭했는지  검사
2. 모달 내부와 외부를 구분하기 위해 내부를 가리키는 모달 요소를 갖고 와야 함
3. 모달이 열리면 브라우저 전체에 모달 닫기 이벤트 등록
4. 모달이 닫히면 브라우저 전체에 등록했던 이벤트 제거

이후 검색을 위해 엔터를 눌렀다거나 

값이 빈 값일 때는 연관 검색어가 안 보이게 해야 한다.

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    
    ...
    
    setIsShow(false)
  }

  const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const query = e.target.value

    if (query === '') {
      setIsShow(false)
      return
    }

  }

 

 

2. react-query를 사용하자!

기존에는 일단 테스트를 위해 fetch로 바로 값을 갖고 와서 진행했다.

이제는 react query를 이용해 isLoading을 활용하려고 한다.

 

처음에 요청은 refetch를 사용해야 하나?라고 생각했는데

쿼리 키값을 state에 저장하고, state를 아래처럼 useQuery에 연결하면 state가 바뀔 때 자동으로 데이터를 가져왔다.

  const { data, isLoading } = useGetHerbSearchList(searchQuery)

그래서 input 값이 바뀔 때마다 setSearchQuery를 해줘서 state값을 바꾸고, 이후 useQuery 요청을 한 다음

isLoading이 false가 됐을 때 연관 검색어를 띄워주었다.

https://i-ten.tistory.com/272

지금 위에서 말한 흐름이 중요하다.

만약 꼬이게 되면 useQuery에서는 계속 이전 연관 검색 결과를 갖고 올 수 또 있다.

 

이때 modal 기능도 추가해야 했기에 isShow도 추가해 주었다.

const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const query = e.target.value

    if (query === '') {
      setIsShow(false)
      setSearchQuery(() => query)
      return
    }
    setSearchQuery(() => query)
    setIsShow(true)
  }
  
  ...
  
  {isShow && (
        <RecommendListUl ref={modalRef}>
          <RecommendListTitle>추천 검색어</RecommendListTitle>

          {isLoading ? (
            <RecommendResultStatus>
              <Spinner />
            </RecommendResultStatus>
          ) : data.length > 0 ? (
            data.map((data: RecommendListItem) => (
              <RecommendListLi key={data.No}>
                <Link to={`/picture?name=${data.name}`}>
                  {boldSearchQuery(data.name, searchQuery)}
                </Link>
              </RecommendListLi>
            ))
          ) : (
            <RecommendResultStatus>
              <p>없음</p>
            </RecommendResultStatus>
          )}
        </RecommendListUl>
      )}

 

예상됐던 문제?

이때 나는 useQuery를 컴포넌트 내 최상위에서 선언했다.

그래서 이 부분에서 예상되는 문제가 있었다.

페이지 어느 곳에나 상단에 떠있는 검색바 특성상 계속해서 useQuery를 호출하지 않을까? 하는 것이었다.

예상대로 위처럼 만약 새로고침을 하면 불필요한 요청을 한번 하게 된다.

하지만 검색어 빈 값을 한번 요청하면 이후에는 데이터가 캐싱되어 요청이 생략되기 때문에 상관없지 않나?라고 생각하게 되었다.

그래도 나중에는 빈 값일 때는 아예 요청되지 않도록 해야 할 것 같았다.(어쨌거나 불필요한 요청이기 때문에)

 

 

3. Input에 value를 연결해야 되는 것일까?

구현 중에 input 태그의 value 속성에 state를 연결하려고 했는데 input에 타이핑이 안 됐고,

콘솔로그로 확인해 보니 state에도 값이 제대로 들어가지 않았다.

나는 내부에서 setState(e.target.value)만 하면 될 줄 알았는데 

해당 코드에서는 디바운스를 사용하고 있었다.

이것이 문제였다.

 

그래서 이 부분을 검색해 보니 아래와 같이 설명하고 있었다.

React의 setState 함수는 비동기로 동작합니다.
따라서 handleChange 함수에서 setState를 호출한 이후에 해당 state 값이 바로 업데이트되지 않을 수 있습니다.
비동기적인 특성으로 인해 state가 업데이트되기 전에 비동기 로직이 실행되면,
state 값이 업데이트되기 전의 값이 사용될 수 있습니다.

내가 커스텀으로 만든 useDebouce는 일정 시간 이후에 onChange 함수가 실행되도록 했다.

그래서 내가 타이핑을 하면

일정 시간 이후에 e.target.value를 통해 값을 가져오게 된다.

근데 value를 input에 등록하게 되면 state는 일정 시간 이후에 setstate가 되므로

이전 state였던 빈 값이 계속해서 state에 저장돼 타이핑을 해도 먹히지 않는 것이다.

 

그니까 간단히 말하면

1. input에 타이핑을 한다.

2. Debouce가 발생한다. -> 이때 비동기 요청이 들어가기 때문에 setState가 바로 일어나지 않는다.

3. input에 value로 등록된 state는 setState가 일어나지 않아서 그대로 빈 값이 된다.

4. 일정시간 이후 Debouce에 등록한 onChage함수를 실행한다.

5. onChange 내에서 setState를 수행한다.

6. 하지만 e.target.value는 현재 input의 value를 가리키는데 value는 3번에서 이미 빈 값이다.

7. 빈 값을 setStaet 하게 된다.

8. 아무 변화 없이 종료

 

setState에 비동기가 추가되면 항상 이 부분도 유의해야 되는 것을 알 게 됐다.

 

4. 검색 결과를 어떻게 보여줘야하지? 쿼리스트링?

이제 검색을 수행했을 때 검색결과를 나타내야 했다.

보통 검색결과는 쿼리스트링을 이용하는 부분을 기준으로 기능을 구현했다.

그래서 react router의 useSearchParams를 이용했다.

const [searchParams] = useSearchParams()
  const searchQuery = searchParams.get('name')
  const {
    herbList,
    isFetching,
    isFetchingNextPage,
    hasNextPage,
    fetchNextPage
  } = useGetHerbList({ searchData: searchQuery })

...

기존에 데이터 요청을 했던 useInfiniteQuery에도 키 값과 검색 데이터를 추가해줘야한다.

 const useGetHerbList = ({
  searchData = '',
  pageNo = 1,
  numOfRows = 10
}: HerbListPageParams) => {
 const { data, hasNextPage, isFetching, isFetchingNextPage, fetchNextPage } =
    useInfiniteQuery<HerbList>({
     queryKey: ['herb', { searchData, pageNo, numOfRows }],
      queryFn: ({ pageParam = pageNo }) =>
        getHerbList({ searchData, pageNo: pageParam as number, numOfRows }),
        
        ...

 

검색을 했을 때 아래처럼 직접 쿼리스트링을 이어줬다.

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    navigate(`/picture?name=${e.currentTarget.search.value}`)
    setIsShow(false)
}

...

<Link to={`/picture?name=${data.name}`}>
  {boldSearchQuery(data.name, searchQuery)}
</Link>

 

이때 submit에서 검색 데이터를 기존에 저장했던 state를 이용하는게 아닌

Form 이벤트에 저장된 event.currentTarget을 이용했다.

 

처음에는 state를 넣어서 구현하려고 했더니 setState가 위에서 말했던 것처럼 Debounce가 추가되어서

만약 빠르게 검색어를 입력하고 검색을 요청하면 

state에서는 Debounce를 진행 중일 때 검색이 된 것이므로 빈 값이 들어가거나 이전 값이 들어가게 됐다.

 그래서 이러한 부분을 해결하기 위해서 event 값으로 검색을 하게 했다.

 

 

 

느낀 점

위 기능들을 구현하면서 많은 분기가 있었고, 그것들을 유의해서 코드를 작성해야했다.

그래서 시간이 많이 소요됐다.

 

사실 전체적으로 보면 그렇게 어려운 로직은 없었다.

기존에 사용했던 fetch를 react-query로 바꾸는 과정이나

분기처리 테스트에서 시간이 많이 걸렸다.

 

연관 검색어라는게 많은 경우의 수가 있어서 막아주거나 분기처리를 해야해서 그런 것 같았다.

 

아 그리고  useQuery에서 queryFn을 쓸 때 아래처럼 두 가지 경우가 있다.

queryFn: () => getHerbList({ searchData: query }),

queryFn: getHerbList,

이 경우의 차이점은 인자가 들어가느냐 안들어가느냐이다.

queryFn에서 ()=>에 유용한 값도 가져올 수 있는데 아직 사용해보지는 못했다.

728x90
반응형
LIST