검색 페이지 요구사항
1. 검색 페이지에 맨 처음 진입 시 사용자들이 제일 많이 검색한 위스키 리스트 Top 10을 출력한다.
2. 사용자가 입력한 값이 있을 경우 해당 입력값을 기반으로 입력값이 포함된 위스키 리스트를 출력한다.
3. 사용자가 값을 입력하고 검색 버튼을 클릭했을 경우 바로 해당 키워드가 포함된 위스키 리스트를 출력한다.
4. input에 입력된 값이 아무것도 없을 경우 다시 위스키 리스트 Top 10을 출력한다.
5. 리스트에 출력된 위스키들은 각각 클릭 시 해당 위스키 상세 페이지로 이동한다.
인기 검색어 구현하기
인기 검색어 추적은 사용자가 검색어를 입력하거나 또는 검색 페이지에서 상세 페이지로 넘어갈 때마다 해당 값을 count하는 로직을 백엔드에서 구현한다. 해당 API 호출 시 인기 검색어 Top10 리스트를 화면에 출력하기만 하면 된다!
// SearchPage.jsx
// 인기검색어 useState
const [trendingList, setTrendingList] = useState(null);
const location = useLocation();
const url = location.pathname;
// searchPage.jsx를 다른 페이지에서도 사용하고 있어 url에 따라 구분해주었음
useEffect(() => {
if (url === '/SearchPage' && data) {
setTrendingList(data);
}
}, [data]);
return (
<Layout>
<SearchInput
searchtype={'after'}
value={userInput}
onchange={onUserInputChangeHandler}
onclick={onSearchClickHandler}
placeholder={'위스키를 검색해보세요!'}
/>
// 인기검색어 리스트는 있고 추천 검색어 리스트가 없거나,
// 사용자가 input에 입력한 값이 없을 때만 인기 검색어 리스트를 출력
{((trendingList && !recommendList) || !userInput) && (
<>
<TrendingH1>{'인기검색어 Top 10'}</TrendingH1>
<DetailList list={trendingList} />
</>
)}
{recommendList && <DetailList list={recommendList} />}
</Layout>
);
};
추천 검색어 구현하기
디바운싱이란?
디바운싱은 짧은 시간 간격으로 연속해서 이벤트가 발생했을 때 과도한 이벤트 핸들러 호출을 방지할 수 있는 기법 중 하나다. 입력 장치에서 발생하는 중복 신호를 방지하기 위한 기법으로, 입력 신호가 반복해서 발생할 때 이를 무시하고 최종 입력 신호를 받아들이도록 한다.
따라서 짧은 시간 간격으로 연속해서 이벤트가 발생하면 이벤트 핸들러를 호출하지 않다가 마지막 이벤트로부터 일정 시간이 경과된 후에 한 번만 호출하도록 한다.
디바운싱을 사용해야 하는 이유
사용자가 input에 입력한 값을 기반으로 해당 입력값이 들어간 추천 검색어를 계속 보여줘야 한다. 사용자가 '짐빔'을 키보드로 입력할 경우 input의 onChange는 'ㅈ, 지, 짐, 짐ㅂ, 짐ㅂㅣ, 짐빔'과 같이 입력값 하나하나를 모두 감지한다. 모든 입력값에 해당 입력값을 기반으로 추천 검색을 조회하는 것은 불필요한 리소스 낭비이므로 디바운싱을 이용해 일정 ms가 지났을 때의 값만을 이용해 추천 검색어 API를 호출한다. (요구사항이 자/모음 단위로 실시간 추천 검색어를 보여준다면 디바운싱을 이용하지 않아도 된다.)
디바운싱을 이용한 추천 검색어 기능
lodash 등의 라이브러리를 이용해도 되지만, 디바운싱이 어떻게 진행되는지 그 흐름을 파악하기 위해 직접 코드를 구현하는 방식을 택했다.
전체 코드 한번에 보기
// * [추천검색어] 커스텀 디바운스 함수
const debounce = (callback, delay) => {
let timerId = null;
// * 3. 리턴문에 의해 아래의 함수를 반환
return (...args) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
callback(...args);
}, delay);
};
};
// * [추천검색어] 2. 디바운스 함수 호출
const setKeywordData = useCallback(
debounce((inputText) => {
setKeyword(inputText);
}, 1000),
[],
);
// * [추천검색어] 1. input 값이 바뀔 때 userInput과 디바운스 함수에 해당 값 전달
const onUserInputChangeHandler = (e) => {
if (e.target.value.length === 0) {
setUserInput('');
setKeywordData('');
setRecommedList(null);
}
setUserInput(e.target.value);
setKeywordData(e.target.value);
};
// * [추천검색어] 조회 useMutation
const searchKeywordMutation = useMutation(getKeywordList, {
onSuccess: (response) => setRecommedList(response),
});
// * [추천검색어] 조회 useEffect : keyword값이 변동될 때마다 디바운싱 거쳐 추천검색어 조회
useEffect(() => {
const searchKeywordHandler = async () => {
if (!/^[ㄱ-ㅎ가-힣0-9]*$|^[a-zA-Z0-9]*$/g.test(userInput)) {
toast.error(`검색어는 한글과 영문을 혼합하거나 특수문자, 공백을 포함할 수 없습니다.`);
} else if (keyword) {
searchKeywordMutation.mutate(keyword);
}
};
searchKeywordHandler();
}, [keyword]);
세부 코드 보기
1. input의 onChangeHandler
// * useState
// 사용자가 input에 실시간으로 입력하는 값
const [userInput, setUserInput] = useState('');
// 사용자가 입력한 키워드 useState
const [keyword, setKeyword] = useState('');
// 추천 검색어 결과 리스트 useState
const [recommendList, setRecommedList] = useState(null);
// 인기 검색어 결과 리스트 useState
const [trendingList, setTrendingList] = useState(null);
// * onChangeHandler
const onUserInputChangeHandler = (e) => {
if (e.target.value.length === 0) {
setUserInput('');
setKeywordData('');
setRecommedList(null);
}
setUserInput(e.target.value); // set useState
setKeywordData(e.target.value); // call function
};
input에 입력한 값(e.target.value)이 없다면 input의 입력값과 키워드 useState를 초기화하고(''), 추천검색어 리스트도 다시 null로 설정한다. input에 입력한 값이 있을 경우 input과 키워드 useState에 해당 값을 넣는다. 이후 setKeyworkData 함수를 호출한다.
2. 디바운스 함수 호출 후 디바운스 함수 실행
// * 디바운스 함수 호출
const setKeywordData = useCallback(
debounce((inputText) => {
setKeyword(inputText);
}, 1000),
[],
);
// * 커스텀 디바운스 함수
const debounce = (callback, delay) => {
let timerId = null;
// * 리턴문에 의해 아래의 함수를 반환
return (...args) => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
callback(...args);
}, delay);
};
};
setKeywordData 함수 호출 시 debounce 함수를 호출한다. debounce 함수는 keyword와 설정한 delay 시간을 매개변수로 받는다. 이후 내부의 setTimeout을 실행해 그 값을 return한다.
키워드 검색 구현하기
// SearchPage.jsx
// * 키워드 검색
const onSearchClickHandler = () => searchKeywordMutation.mutate(userInput);
// * 조회 useMutation
const searchKeywordMutation = useMutation(getKeywordList, {
onSuccess: (response) => setRecommedList(response),
});
return (
<Layout>
<SearchInput
searchtype={'after'}
value={userInput}
onchange={onUserInputChangeHandler}
onclick={onSearchClickHandler}
placeholder={'위스키를 검색해보세요!'}
/>
// 인기검색어 리스트는 있고 추천 검색어 리스트가 없거나,
// 사용자가 input에 입력한 값이 없을 때만 인기 검색어 리스트를 출력
{((trendingList && !recommendList) || !userInput) && (
<>
<TrendingH1>{'인기검색어 Top 10'}</TrendingH1>
<DetailList list={trendingList} />
</>
)}
{recommendList && <DetailList list={recommendList} />}
</Layout>
);
};
사용자가 검색어를 입력한 직후 조회 아이콘을 클릭해 즉각적인 키워드 검색 기능을 이용할 경우, input의 onChangeHandler에서 입력값을 기반으로 바로 조회 API를 호출한다.
[참고 자료]
https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa
'Front-End > React' 카테고리의 다른 글
[React] React-router-dom을 이용한 Private Route 설정 시 인증/인가 처리 오류 (0) | 2023.06.03 |
---|---|
[React] 카카오맵(1) 지도 내장 검색 기능 사용해 리스트 출력하기 (0) | 2023.06.01 |
[React] 리액트에서 Styled Components Global Style, Theme 설정하기 (0) | 2023.05.30 |
[React] 리액트에서 Airbnb ESLint + Prettier 설정하기 (0) | 2023.05.27 |
[React] React-icon에 Styled Components 적용하기 (0) | 2023.05.13 |