[React] 검색 기능 구현

이지·2023년 12월 7일
post-thumbnail

요구사항

  • 검색 결과
    • 전체 : 플리, 유저 모두 보여주기
    • 플리 : 플리만 보여주기
    • 유저 : 유저만 보여주기
  • 최근 검색어
    • 최근 검색어 전체 삭제
    • 최근 검색어 선택 삭제
    • 최근 검색어 클릭 시 결과 페이지 보여주기

검색

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

검색기능

<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>
  );
}

0개의 댓글