검색 기능 구현에 있어서 디바운싱(debouncing) 이라는 개념을 적용하였다.
연이어 호출 되는 함수들 중 마지막 함수만 호출 하도록 하는 것으로 주로 ajax 검색에 사용 된다.
검색 창에 검색어를 입력 시 바로 결과를 보여주려면 검색 창 input에 onChange(or onInput, React에서는 onChange도 onInput처럼 작동 됨. 필자는 프로젝트에 onChange를 사용하였다.) 이벤트로 검색 값을 호출하는 함수를 실행하게 끔 걸어주어야 한다. 이렇게 되면 input 값이 변할 때 마다 호출이 되는데, 사용자가 검색어를 다 입력 하지 못했는데도 호출이 계속 되는 현상이 나타난다.
ex) 사용자가 ‘galaxy’를 입력하고자 하는 상황 - ‘g’ ,’ga’,’gal’,’gala’,’galax’,’galaxy’ 총 6번의 호출이 요청 됨.
이런 호출 방식은 비효율적인 방식으로, 유료 API일 경우는 비용 적인 문제도 동반한다.
그래서, 사용자가 입력을 다한 후 요청을 보내기 위한 방법으로 onChange함수가 적용 할 때 마다 타이머를 설정하고( 기존의 타이머가 있다면 삭제) 타이머가 다 끝나고 나면 최종 값을 리턴 하는 것이다.
필자가 적용한 코드는 다음 과 같다.
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;
const SearchPage = () => {
...
const searchParams = useSearchParams();
const searchTerm = searchParams.get('str') || '';
const debouncedTerm = useDebounce(searchTerm, 1000);
const searchMovies = movieAPI.useGetSearchMovieListsQuery(searchTerm, { skip: debouncedTerm ? false : true });
.....
디바운싱 적용에 관해서 찾아보다가 이렇게 적용해도 좋을 것 같다 하는 방법이 있어 추가로 정리해 본다.
react 18 버전 부터 적용이 가능한 useTransition 훅을 사용 하는 것이다.
useTransition은 다음과 같이 선언한다.
const [isPending, startTransition] = useTransition()
기본
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>
);
}
응용
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>
)
}
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>
)
}