검색창 구현 + 검색어 추천 기능 구현 + 캐싱 기능 구현
npm install
npm start
🔗 배포 링크
김대연 |
김용희 |
박상민 |
윤예나 |
이상돈 |
임예지 |
장은영 |
조승현 |
진호병 |
sessionStorage
는 세션이 종료되면 (e.g. 브라우저 닫기) 저장한 데이터가 지워지므로 임시적으로 사용하는 데이터를 저장하기 적합합니다.localStorage
는 사용자가 저장된 데이터를 직접 삭제하지 않는 한 영구적으로 보존되기 때문에 사이트를 재방문할 때 사용할 수 있는 데이터를 활용하기 적합합니다.- 이번 과제를 구현하며 브라우저를 닫았다 다시 방문해도 이전 검색어가 남아있는 것이 낫다는 의견으로 모아져
localStorage
를 사용하기로 결정했습니다.
const SearchApi = axios.create({
method: "GET",
baseURL: "/api/v1/search-conditions",
});
SearchApi.interceptors.request.use((config) => {
console.info("calling api");
return config;
});
SearchApi.interceptors.response.use((response) => {
if (response.status !== 200) {
throw new Error("api 호출이 실패하였습니다.");
}
if (response.data.length === 0) {
console.info("api 호출이 실패하였습니다.");
}
const url = new URLSearchParams(`${response.config.url}`);
const key = url.get("name");
localStorage.setItem(
key,
JSON.stringify({
value: response.data,
expiresAt: Date.now() + 1000 * 60 * 5, // 5분
})
);
return response.data;
});
- Axios Instance를 생성하고
Interceptors
를 사용해서 API 호출 후 성공적으로 받은 응답 데이터를localStorage
에 저장하며 캐싱 과정을 간소화했습니다.
const checkExpiredCache = () => {
Object.keys(localStorage).forEach((key) => {
const obj = localStorage.getItem(key);
const searchValueObj = JSON.parse(obj);
if (Date.now() > searchValueObj.expiresAt) {
localStorage.removeItem(key);
}
});
};
checkExpiredCache
함수를 사용해 expire time을 체크하고 로컬스토리지에서 제거했습니다.
- 검색창에서 추천 검색어 API를 구현하는 경우:
Debounce
는 입력이 멈추고 정해진 시간 후에 API를 호출하게 됩니다.Throttle
은 입력이 진행되는 동안 일정한 주기로 API를 호출하게 됩니다.
Throttle
을 사용하면 검색어 입력동안 주기적으로 업데이트되는 추천 검색어로 인해 사용자의 측면을 더 고려하며,Debounce
를 사용하면 입력이 끝나고 일정 시간 후 업데이트되기 때문에 성능적인 측면을 더 고려하게 됩니다.- 이번 과제에서는 요구 사항에 맞춰 호출 횟수를 줄이는 전략을 수립하기 위해
Debounce
사용했습니다.
// src/components/SearchBar.jsx
useEffect(() => {
const getCachedDataOrFetch = async () => {
if (!searchName) return setSuggestions([]);
checkExpiredCache();
const caches = JSON.parse(localStorage.getItem(searchName));
if (caches) return setSuggestions(caches.searchValue);
const searchResult = await fetchResults(searchName);
setSuggestions(searchResult);
};
const debounceFetch = setTimeout(() => {
getCachedDataOrFetch();
}, 500);
return () => {
clearTimeout(debounceFetch);
};
}, [searchName, caches]);
Debounce
를 사용한 검색 최적화- 500ms의 지연 시간을 설정하여, 사용자가 입력을 완료할 때까지 요청을 지연시켰습니다.
- 이를 통해 불필요한 검색 요청을 줄이고 성능을 최적화할 수 있었습니다.
// src/components/SearchBar.jsx
if (!searchName || RegExp(searchName)) return setSuggestions([]);
// src/hooks/useSuggestionFocus.jsx
import { useState } from "react";
const KEY = {
ArrowDown: "ArrowDown",
ArrowUp: "ArrowUp",
Enter: "Enter",
BackSpace: "BackSpace",
Delete: "Delete",
};
export default function useSuggestionFocus(
suggestions,
setSearchName,
setOpenModal,
searchRef
) {
const [focusIdx, setFocusIdx] = useState(-2);
const suggestionLength = suggestions.length;
const changeIdxNum = (e) => {
const key = e.key;
if (key === KEY.Escape) {
setOpenModal(false);
setSearchName("");
}
if (suggestionLength > 0) {
searchRef.current?.scrollIntoView({
behavior: "smooth",
block: "center",
});
if (key === KEY.ArrowDown) {
e.preventDefault();
setFocusIdx((prev) => (prev + 1) % suggestionLength);
}
if (key === KEY.ArrowUp) {
e.preventDefault();
setFocusIdx((prev) => (prev - 1 + suggestionLength) % suggestionLength);
}
if (key === KEY.Enter) {
if (focusIdx >= 0) {
setSearchName(suggestions[focusIdx].name);
setFocusIdx(-1);
}
}
if (key === KEY.BackSpace || key === KEY.Delete) {
setFocusIdx(-1);
}
}
};
return {
changeIdxNum,
focusIdx,
setFocusIdx,
};
}
- 화살표 키를 사용하여, 추천 검색어 목록 탐색
- 위 방향키를 누를 경우, focusIdx가 1 씩 증가합니다. 만약 가장 위의 항목에 도달하여 위 방향키를 누르면, 포커스는 가장 아래 항목으로 이동합니다.
- 마찬가지로, 아래쪽 방향키를 눌러 만약 가장 아래 항목에 도달하여 아래쪽 방향키를 누르면, 포커스는 가장 위 항목으로 이동합니다.
- 포커스가 있는 상태에서 Enter 키를 누르면, 선택된 항목의 이름이 검색어로 설정됩니다.
// src/components/SearchSuggestionModal.jsx
<SearchSuggestionListItem
key={suggestion.id}
name={suggestion.name}
focus={focusIdx === idx}
handleMouseOver={() => setFocusIdx(idx)}
handleMouseOut={() => setFocusIdx(-2)}
setSearchName={setSearchName}
searchRef={searchRef}
/>
// src/components/SearchSuggestionListItem.jsx
<StyledSearchSuggestionListItem
focus={focus}
onClick={modalOutSideClick}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
>
</StyledSearchSuggestionListItem>
-
마우스 호버와 키보드 포커스 연동 기능
- 마우스로 항목에 호버할 경우, 해당 항목에 포커스가 됩니다.
- 마우스가 항목에서 벗어날 경우, 포커스가 초기화됩니다.
- 이를 통해 사용자가 마우스와 키보드를 동시에 사용하여 추천 검색어 목록에서 원하는 항목을 선택합니다.
// src/hooks/useSuggestionFocus.jsx
searchRef.current?.scrollIntoView({
behavior: "smooth",
block: "center",
});
// src/components/SearchSuggestionListItem.jsx
<StyledSearchSuggestionListItem
focus={focus}
onClick={modalOutSideClick}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
ref={focus ? searchRef : null}
></StyledSearchSuggestionListItem>;
- 키보드로 추천 검색어 스크롤 시 뷰포인트 이동
- scrollIntoView 활용하여 키보드로 추천 검색어 이동 시 포커스가 마지막에 가기 전에 다음 목록을 보여줍니다.
- focus된 StyledSearchSuggestionListItem에 ref를 지정하여 foucs된 검색어를 지정하도록 했습니다.
타입 | 내용 |
---|---|
Feat | 새로운 기능 추가 |
Fix | 버그 수정 |
Env | 개발 환경 관련 설정 |
Style | 코드 스타일 수정 (세미 콜론, 인덴트 등의 스타일적인 부분만) |
Refactor | 코드 리팩토링 (더 효율적인 코드로 변경 등) |
Design | CSS 등 디자인 추가/수정iE |
Comment | 주석 추가/수정 |
Docs | 내부 문서 추가/수정 |
Test | 테스트 추가/수정 |
Chore | 빌드 관련 코드 수정 |
Rename | 파일 및 폴더명 수정 |
Remove | 파일 삭제 |
e.g. Feat: 검색 기능 수정
📦 src
├── 📂 api
├── 📂 assets
├── 📂 components
│ ├── 📄 HeaderTitle
│ ├── 📄 SearchBar
│ ├── 📄 SearchButton
│ ├── 📄 SearchSuggestionListItem
│ ├── 📄 SearchSuggestionModal
├── 📂 hooks
├── 📂 pages
├── 📂 styles
├── 📂 utils
├── 📄 App
└── 📄 index