5. 검색 디바운싱

mangojang·2023년 6월 7일
0

검색 기능 구현에 있어서 디바운싱(debouncing) 이라는 개념을 적용하였다.

디바운싱(debouncing)이란?

연이어 호출 되는 함수들 중 마지막 함수만 호출 하도록 하는 것으로 주로 ajax 검색에 사용 된다.

검색 창에 검색어를 입력 시 바로 결과를 보여주려면 검색 창 input에 onChange(or onInput, React에서는 onChange도 onInput처럼 작동 됨. 필자는 프로젝트에 onChange를 사용하였다.) 이벤트로 검색 값을 호출하는 함수를 실행하게 끔 걸어주어야 한다. 이렇게 되면 input 값이 변할 때 마다 호출이 되는데, 사용자가 검색어를 다 입력 하지 못했는데도 호출이 계속 되는 현상이 나타난다.

ex) 사용자가 ‘galaxy’를 입력하고자 하는 상황 - ‘g’ ,’ga’,’gal’,’gala’,’galax’,’galaxy’ 총 6번의 호출이 요청 됨.

이런 호출 방식은 비효율적인 방식으로, 유료 API일 경우는 비용 적인 문제도 동반한다.

그래서, 사용자가 입력을 다한 후 요청을 보내기 위한 방법으로 onChange함수가 적용 할 때 마다 타이머를 설정하고( 기존의 타이머가 있다면 삭제) 타이머가 다 끝나고 나면 최종 값을 리턴 하는 것이다.

적용한 코드

필자가 적용한 코드는 다음 과 같다.

src> hooks> useDebounce.tsx

  • useEffect를 사용, setTimeout으로 타이머를 적용하여 값을 바꿔준다.
  • useEffect 의 return 값으로 clearTimeout을 호출 → 이전에 적용 된 타이머(handler 함수)를 제거
import React, { useEffect, useState } from 'react';

const useDebounce = (value: any, delay: number): string => {
	const [debounceValue, setDebounceValue] = useState('');
	useEffect(() => {
		const handler = setTimeout(() => {
			setDebounceValue(value);
		}, delay);

		return () => {
			clearTimeout(handler);
		};
	}, [value, delay]);
	return debounceValue;
};

export default useDebounce;

src>pages>search>index.tsx

  • RTK query의 useGet…Query 훅의 skip 옵션을 useDebounce의 결과값인 debouncedTerm으로 컨트롤 하여 lazyQuery를 적용함.
  • lazyQuery: 컴포넌트가 생성 됨과 같이 data fetch가 실행되는 것이 아닌, 특정 시점에 data fetch가 실행 되게 하는 방법.
const SearchPage = () => {
...
	const searchParams = useSearchParams();
	const searchTerm = searchParams.get('str') || '';
	const debouncedTerm = useDebounce(searchTerm, 1000);
	const searchMovies = movieAPI.useGetSearchMovieListsQuery(searchTerm, { skip: debouncedTerm ? false : true });
.....

다른 방법

디바운싱 적용에 관해서 찾아보다가 이렇게 적용해도 좋을 것 같다 하는 방법이 있어 추가로 정리해 본다.

useTransition

react 18 버전 부터 적용이 가능한 useTransition 훅을 사용 하는 것이다.

useTransition은 다음과 같이 선언한다.

const [isPending, startTransition] = useTransition()
  • isPending: startTransition 이 적용된 상태 변경이 현재 pending 상태인지여부를 알려줌.
  • startTransition: 상태 업데이트에 transition을 적용함. → 상태 업데이트를 낮은 우선순위로 실행 함. = 더 중요한 이벤트가 있는 경우, startTransition 으로 감싼 이벤트를 지연 시키고 대신 이전의 값을 보여줌.

기본

function App() {
  const [loading, startTransition] = useTransition();
  const [count, setCount] = useState(0);
  
  function handleClick() {
		// setCount의 우선순위를 낮춤. -> 연속으로 눌러도 그 누른 값들이 누를때 마다 바로바로 적용되지 않음.
    startTransition(() => {
      setCount(c => c + 1);
    })
  }
  
  return (
    <div>
			// 상태 변경이 완료 되면 Component render
      {loading && <Component />}
      <button onClick={handleClick}>{count}</button>
    </div>
  );
}

응용

  • TextInput 컴포넌트를 따로 분리한 이유는 입력을 제어하는 상태변수(text) 에는 startTransition을 적용 할 수 없다고 한다. - 공식문서 참고
  • 해결 방법으로 두 개의 개별 상태변수를 선언하여 하나는 동기적으로 (text, setText), 하나는 렌더 될 시 전달 되는 논리 (onChange-App의 handleChange)로 뒤처짐을 주는 것이다.

function TextInput({onChange}){

	const [text,setText] = useState('');

	return(
		<div>
			<input
				type="text"
				value={text}
				onChange={({target})=>{
					setText(target.value)
					onChange(target.value)
				}}
		</div>
	)
}

function App(){
	const [size, setSize] = useState(0);
	const [isPending, startTransition] = useTransition();
	
	function handleChange(text){
		startTransition(()=>{
			setSize(text.length)
		})
	}

	return(
		<div>
			<h1> Concurrent ({size})</h1>
			<TextInput onChange={handleChange}/>
			{isPending? <ColorList length={size}/> : ''}
		</div>
	)
}

useLazyQuery

RTK query 에서 제공하는 훅으로 trigger 함수와 result 를 반환한다. trigger함수를 사용하면 data가 fetch 되어 result에 담긴다.

const [trigger, result] = useLazyGetUsersQuery(); // useLazy...Query로 명명한다.

예시

api.js

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const api = createApi({
    reducerPath: "api",
    baseQuery: fetchBaseQuery({baseUrl: "https://jsonplaceholder.typicode.com/"}),
    endpoints: (builder) => ({
        getUsers: builder.query({
            query: (user) => `users/${user}`
        }),
    })
});

// Add Lazy after "use" to convert it into Lazy Query hook
export const { useLazyGetUsersQuery } = api;

App.js

import { useEffect, useState } from "react";
import { useLazyGetUsersQuery } from "./redux/api";

function App() {
  const [userData, setUserData] = useState();
  // Returns trigger function and results object
  const [getUsers, results] = useLazyGetUsersQuery();

  useEffect(() => {
    if(results && results.data) {
      setUserData([results.data]);
    }
    console.log(results)
  },[results])

  return (
    <div className="App">
      {userData && userData.map(item => (
        <div key={item.id}>
          <p>{item.name}</p>
          <p>{item.email}</p>
        </div>
      ))}
			// 버튼 클릭시, trigger 함수 실행
      <button onClick={() => getUsers(1)}>Fetch User</button>
    </div>
  );
}

export default App;

응용

위의 예시들을 바탕으로 useTransition 과 useLazyQuery를 사용해 검색기능을 구현하면 다음과 같게 적용 할 수 있을 것 같다.

api.js

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const api = createApi({
    reducerPath: "api",
    baseQuery: fetchBaseQuery({baseUrl: "api 주소"}),
    endpoints: (builder) => ({
        getSearchData: builder.query({
            query: (str) => `search?str=${str}`
        }),
    })
});

// Add Lazy after "use" to convert it into Lazy Query hook
export const { useLazyGetSearchDataQuery } = api;

App.js

function TextInput({onChange}){

	const [text,setText] = useState('');

	return(
		<div>
			<input
				type="text"
				value={text}
				onChange={({target})=>{
					setText(target.value)
					onChange(target.value)
				}}
		</div>
	)
}

function App(){
	const [searchStr, setSearchStr] = useState('');
	const [searchResults, setSearchResults] = useState('');
	const [isPending, startTransition] = useTransition();
	const [trigger, results] = useLazyGetSearchDataQuery();

	useEffect(()=>{
		searchStr && trigger(searchStr)

	},[searchStr])

	useEffect(() => {
    if(results && results.data) {
      setSearchResults([results.data]);
    }
    console.log(results)
  },[results])

	
	function handleChange(text){
		startTransition(()=>{
			setSearchStr(text)
		})
	}

	return(
		<div>
			<h1> Concurrent ({size})</h1>
			<TextInput onChange={handleChange}/>
			{setSearchResults&& <SearchResults/>}
		</div>
	)
}

참고 문헌

profile
한 걸음 한 걸음 계속 걷는 자가 일류다

0개의 댓글