요구사항
검색 페이지에서는 검색어를 입력하게 되면 그 결과를 전체, 플리, 유저에 따라 나눠서 보여주어야 합니다.

<Search.jsx>
export default function Search() {
const [result, setResult] = useState(); // 검색 결과를 담을 state
const [currentNav, setCurrentNav] = useState({ // nav의 type을 담은 state
all: true,
playlist: false,
user: false,
});
const [inputValue, setInputValue] = useState(''); // 입력된 검색어
const SearchSubmit = (e) => {
e.preventDefault();
getSearchData(inputValue);
handleAddRecentKeyword(inputValue);
};
const getSearchData = async (query) => { // API 통신
try {
const res = await privateInstance.get(`/playlist/search/?query=${query}`);
setResult(res.data);
} catch (err) {
console.error(err.response.data);
}
};
return (
<S.SearchWrap>
{/* 검색어 입력창 */}
<SearchInput
setResult={setResult}
setInputValue={setInputValue}
onSubmit={SearchSubmit}
onAddRecentKeyword={handleAddRecentKeyword}
/>
{/* 검색 결과 (검색 결과가 있어야 보임)*/}
{result && (
<>
<SearchNav currentNav={currentNav} setCurrentNav={setCurrentNav} />
{currentNav.all && (
<SearchResultAll result={result} setCurrentNav={setCurrentNav} />
)}
{(currentNav.playlist || currentNav.user) && (
<SearchResultByType result={result} currentNav={currentNav} />
)}
</>
)}
</S.SearchWrap>
);
}
검색(전체)에서는 플리와 유저의 결과를 최대 3개까지 보여주어야 합니다.
백엔드에서 이를 구분해서 전달해주기 때문에 전체 데이터를 자르지 않고 사용할 수 있었습니다.
<SearchResultAll.jsx>
export default function SearchResultAll(props) {
const { result, setCurrentNav } = props;
const maskedEmail = (email) => {
return email.replace(/@.*/, '');
};
const handleNavPlaylist = () => {
setCurrentNav({ all: false, playlist: true, user: false });
};
const handleNavUser = () => {
setCurrentNav({ all: false, playlist: false, user: true });
};
return (
<>
<SearchListBox>
<SearchListSection>
<SearchListTitleBox>
<h2>플리 검색결과</h2>
<button onClick={handleNavPlaylist}>
<ArrowIcon fill='black' />
</button>
</SearchListTitleBox>
<PlayList>
{result.recent_playlists.length !== 0 ? (
result.recent_playlists.map((item) => {
return (
<PlayListItem
key={item.playlist.id}
img={item.playlist.thumbnail}
title={item.playlist.title}
info={item.writer.name}
></PlayListItem>
);
})
) : (
<EmptySearch />
)}
</PlayList>
</SearchListSection>
<SearchListSection>
<SearchListTitleBox>
<h2>유저 검색결과</h2>
<button onClick={handleNavUser}>
<ArrowIcon fill='black' />
</button>
</SearchListTitleBox>
<UserList>
{result.recent_users.length !== 0 ? (
result.recent_users.map((user) => {
return (
<UserItem key={user.id}>
<UserImgBox>
<CircleImage src={user.image} alt='유저이미지' />
</UserImgBox>
<UserInfoBox>
<div>{maskedEmail(user.email)}</div>
<p>{user.name}</p>
</UserInfoBox>
</UserItem>
);
})
) : (
<EmptySearch />
)}
</UserList>
</SearchListSection>
</SearchListBox>
</>
);
}
<SearchResultByType.jsx>
export default function SearchResultByType(props) {
const { result, currentNav } = props;
const [type, setType] = useState('');
// 검색(플리), 검색(유저)만 보여주기 위해서 currentNav의 현재 값을 통해 type에 저장하도록 했습니다.
const SearchResultType = () => {
if (result) {
if (currentNav.playlist) setType('playlist');
else if (currentNav.user) setType('user');
}
};
const maskedEmail = (email) => {
return email.replace(/@.*/, '');
};
useEffect(() => {
SearchResultType();
}, []);
return (
<>
{/* 플리 결과만 */}
{type === 'playlist' && (
<PlayList>
{result.playlists.length !== 0 ? (
result.playlists.map((item) => (
<PlayListItem
key={item.playlist.id}
img={item.playlist.thumbnail}
title={item.playlist.title}
info={item.writer.name}
></PlayListItem>
))
) : (
<EmptySearch />
)}
</PlayList>
)}
{/* 유저 결과만 */}
{type === 'user' && (
<UserList>
{result.users.length !== 0 ? (
result.users.map((user) => {
return (
<UserItem key={user.id}>
<UserImgBox>
<CircleImage src={user.image} alt='유저이미지' />
</UserImgBox>
<UserInfoBox>
<div>{maskedEmail(user.email)}</div>
<p>{user.name}</p>
</UserInfoBox>
</UserItem>
);
})
) : (
<EmptySearch />
)}
</UserList>
)}
</>
);
}
보통 최근 검색어 기능을 구현할 때 localStorage를 사용합니다.
백엔드에 요청해서 서버에 저장하는 방법도 있지만 굳이 서버에 저장하지 않아도 된다고 판단하여 localStorage를 활용해서 구현하였습니다.
<Search.jsx>
export default function Search() {
...
const [recentKeywords, setRecentKeywords] = useState(
JSON.parse(localStorage.getItem('recent_keywords')) || [],
);
// 최근 검색어 추가
const handleAddRecentKeyword = (keyword) => {
// 검색어가 이미 존재하는지 확인
const isKeywordExist = recentKeywords.some(
(item) => item.keyword === keyword,
);
let updatedKeywords;
// 이미 검색한 단어인 경우 해당 단어를 배열에서 제거
if (isKeywordExist) {
updatedKeywords = recentKeywords.filter(
(item) => item.keyword !== keyword,
);
} else {
updatedKeywords = recentKeywords;
}
// 새로운 검색어 객체 생성
const newKeyword = {
id: Date.now(),
keyword,
};
// 새로운 검색어를 배열의 맨 앞에 추가하여 최신 검색어로 유지
setRecentKeywords([newKeyword, ...updatedKeywords]);
};
// 최근 검색어 선택 삭제
const handleRemoveRecentKeyword = (id) => {
const nextKeywords = recentKeywords.filter((keyword) => keyword.id !== id);
setRecentKeywords(nextKeywords);
};
// 최근 검색어 전체 삭제
const handleRemoveAllRecentKeyword = () => {
setRecentKeywords([]);
};
// 검색했을 때 로컬스토리지에 저장
useEffect(() => {
localStorage.setItem('recent_keywords', JSON.stringify(recentKeywords));
}, [recentKeywords]);
return (
<S.SearchWrap>
...
{/* 최근 검색어 */}
{!result && recentKeywords.length !== 0 && (
<RecentSearch
recentKeywords={recentKeywords.slice(0, 3)} {/* 최근 검색어 최대 3개까지 보여줌 */}
onRemoveRecentKeyword={handleRemoveRecentKeyword}
onRemoveAllRecentKeyword={handleRemoveAllRecentKeyword}
getSearchData={getSearchData}
/>
)}
...
</S.SearchWrap>
);
}
<RecentSearch.jsx>
export default function RecentSearch({
recentKeywords,
onRemoveRecentKeyword,
onRemoveAllRecentKeyword,
getSearchData,
}) {
return (
<RecentSearchWrap>
<p>최근 검색어</p>
<RecentSearchList>
{recentKeywords.map(({ id, keyword }) => {
return (
<li key={id}>
<div>
<img src={TimePastIcon} alt='최근검색' />
<button onClick={() => getSearchData(keyword)}>
{keyword}
</button>
</div>
<button onClick={() => onRemoveRecentKeyword(id)}>
<img src={CloseIcon} alt='삭제' />
</button>
</li>
);
})}
</RecentSearchList>
<DeleteBtn onClick={onRemoveAllRecentKeyword}>검색어 전체삭제</DeleteBtn>
</RecentSearchWrap>
);
}