[React] 최근본상품, 최근검색어, 자동저장 기능 구현하기

녕녕·2023년 6월 21일
0

React🍰

목록 보기
4/4
post-thumbnail

🔎 레이아웃

검색 페이지검색결과 페이지

검색 페이지의 레이아웃은 위와 같이 짰다.

🔎 검색 페이지 컴포넌트, 최근 본 상품

.
.
//검색바 컴포넌트
import SearchBox from './SearchComponents/SearchBox';
//최근검색어 컴포넌트
import KeywordsList from './SearchComponents/KeywordsList';
.
.

interface StateProps {
  productName: string;
  productId: number;
  category: string;
}

const Search = () => {
  // 최근 본 상품
  const [recentProduct, setRecentProduct] = useState<StateProps | undefined>();

  useEffect(() => {
    const getSeverRecentProductData = async () => {
      const json = await getRecentProduct();
      setRecentProduct(json.slice(-1)[0]);
    };
    getSeverRecentProductData();
  }, []);

  return (
    <Container>
      <SearchBox />
      {recentProduct && (
        <RecentProducts>
          <h4>최근 본 상품</h4>
          <DetailLink to={`/detail/${recentProduct.category.toLowerCase()}/${recentProduct.productId}`}>
            <div>
              <span className="history">
                <BiHistory />
              </span>
              {recentProduct.productName}
            </div>
            <div className="info">
              상세정보
              <span className="move">
                <IoChevronForwardOutline />
              </span>
            </div>
          </DetailLink>
        </RecentProducts>
      )}
      <RecentKeywords>
        <KeywordsList />
      </RecentKeywords>
    </Container>
  );
};
  • 검색바 - <SearchBox />, 최근 본 상품, 최근 검색어 - <KeywordsList /> 세가지 컴포넌트가 순서대로 오게 된다.
  • 검색바와 최근 검색어 컴포넌트
    • 파일을 따로 만들어 import 했다.
  • 최근 본 상품
    • useState로 관리할 state를 선언해준다.
    • useEffect로 검색 페이지 컴포넌트 마운트시, 한번만 서버의 데이터를 가져오는 비동기 api를 실행하도록 한다.
    • 서버 데이터의 상품명을 setState 하여 값의 상태를 갱신해준다.
    • 갱신된 state를 jsx의 원하는 위치에 적어줘, 화면에 렌더링 되도록 한다.
    • 최근 본 상품을 감싸고 있는 DetailLink은 styled-components의 styled(Link)로 Link를 받아, to={이동하려는 위치}를 통해 상품 상세페이지로 이동할 수 있도록 하였다.

🔎 최근 검색어 컴포넌트

const KeywordsList = () => {
  const dispatch = useDispatch();
  const [data, setData] = useState<Array<StateObject>>([]);
  const [toast, setToast] = useState(false);
  const [deletedCount, setDeletedCount] = useState('');
  const location = useLocation();
  const findResultsPage = location.pathname.slice(0, 8) === '/search/';
  const isToggleTrue = useSelector<ReducerType>((state) => state.autosave.isToggleTrue);

  //서버에서 키워드 목록 가져오기
  useEffect(() => {
    const getSeverSearchKeywordsData = async () => {
      const json = await getSearchKeywords();
      setData(json);
    };
    getSeverSearchKeywordsData();
  }, []);

  //키워드 단일 삭제
  const handleDeleteKeyword = (id: number) => {
    const deletedData = data.filter((element) => element.searchId !== id);
    setData(deletedData);
    deleteSearchKeywordsSingle(id);
  };

  //키워드 전체 삭제
  const handleDeleteKeywordAll = async () => {
    if (!confirm('최근 검색어를 모두 삭제하시겠습니까?')) return;
    setData([]);
    const res = await deleteSearchKeywordsAll();

    // 00개 삭제 완료 토스트 띄우기
    res.status === 'success' && setToast(true);
    setDeletedCount(res.deletedNum);
  };
  
  //자동저장 상태를 store로 전달
  const toggleAutosaveHandler = () => {
    dispatch(toggle());
  };
  
  //자동저장
  const handleAutoSave = () => {
    confirm(
      isToggleTrue ? '최근 검색어 저장 기능을\n사용 중지하시겠습니까?' : '최근 검색어 저장 기능을\n사용 하시겠습니까?'
    ) && toggleAutosaveHandler();
  };

  return (
    //검색페에지와 검색결과페이지 스타일 다르게
    <Container className={findResultsPage ? 'resultPage' : ''}>
      <div>
        <h4>최근에 찾아봤던</h4>
        <button className="autoSave" onClick={handleAutoSave}>
          자동저장 {isToggleTrue ? '끄기' : '켜기'}
        </button>
      </div>
      {isToggleTrue ? (
        data && data.length !== 0 ? (
          <>
            <ol>
              {data
                .sort((a, b) => {
                  return +new Date(b.createdAt) - +new Date(a.createdAt);
                })
                .map((list) => (
                  <List key={list.searchId}>
                    <SearchLink to={`/search/${list.searchContent}`}>{list.searchContent}</SearchLink>
                    <button onClick={() => handleDeleteKeyword(list.searchId)}>
                      <TfiClose />
                    </button>
                  </List>
                ))}
            </ol>
            <button className="deleteAll" onClick={handleDeleteKeywordAll}>
              모두 지우기
            </button>
          </>
        ) : (
          <Info>최근 찾아봤던 내역이 없습니다.</Info>
        )
      ) : (
        <Info>검색어 저장 기능이 꺼져있습니다.</Info>
      )}
      <Toast isTrue={toast} message={`${deletedCount}개가 삭제됐어요`} />
    </Container>
  );
};
  • 최근검색어

    • useEffect의 의존성 배열을 비워서, 컴포넌트가 처음으로 렌더링될 때 서버에서 검색 키워드 데이터를 가져와서 상태로 설정한다.
    • 배열의 값이 있으면 sort 메서드를 사용해 최신순으로 정렬하고, 정렬한 값을 보여준다. link로 묶어 클릭시 바로 검색결과 페이지로 넘어가도록 한다.
    • link로 묶은 것 안에 단일 삭제 버튼도 배치하고, 클릭시 filter 메서드를 사용해 클릭한 것 이외의 검색어들만 남아있도록 처리한다. 서버로 삭제 api도 요청한다.
    • 이 컴포넌트는 검색결과 페이지에서 검색결과가 없을 때 재사용된다.
      👉구현 영상
  • 자동저장

    • useDispatch를 사용하여 dispatch 함수를 가져오고, dispatch 함수를 호출하여 toggle() 액션을 디스패치한다. 액션 디스패치를 통해 redux의 상태를 업데이트 한다. redux의 자동저장 상태는 useSelector를 통해 조회한다.
    • 자동저장 상태에 따라 자동저장 켜기 또는 자동저장 끄기로 버튼을 보여줄 수 있다. 버튼을 눌렀을 때는 confirm 창을 띄워주고, 창에서 누른 버튼이 true일 경우에만 액션 디스패치로 redux의 상태를 원래 상태와 반대로 업데이트 한다.
    • 자동저장 상태에 따라 켜져 있으면 최근 검색어 목록을 보여주고, 꺼져있으면 저장 기능이 꺼져있다는 문구를 보여준다.
      👉구현 영상

🔎 검색바 컴포넌트

const SearchBox = () => {
  const isToggleTrue = useSelector<ReducerType>((state) => state.autosave.isToggleTrue);

  const location = useLocation();
  const params = useParams();
  const navigate = useNavigate();
  const [inputValue, setInputValue] = useState('');

  const findResultsPage = location.pathname.slice(0, 8) === '/search/';

  //검색결과 페이지에서 검색바에 키워드 보여주기
  useEffect(() => {
    params.keywords !== undefined && setInputValue(params.keywords);
  }, []);

  //검색결과 페이지에서만 뒤로가기 버튼 보여줌. 검색페이지로 이동하는 버튼.
  const handleBack = () => {
    navigate('/search');
  };

  //검색 submit 되면, 페이지 이동 및 자동저장 여부에 따라 키워드 저장 api 호출
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    const movepageAndAddkeywordAndCallApi = () => {
      navigate(`/search/${inputValue}`);
      isToggleTrue && addSearchKeywords(inputValue);
    };

    inputValue !== '' ? movepageAndAddkeywordAndCallApi() : alert('상품명을 입력해주세요.');
  };

  //키워드가 있을 때만, 키워드삭제 버튼 보여주려고 사용
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(event.target.value);
    console.log(event.target.value);
  };

  //키워드 삭제 버튼
  const handleDeleteBtn = () => {
    setInputValue('');
  };

  return (
    <Container onSubmit={handleSubmit} className={findResultsPage ? 'results' : ''}>
      {/* 검색결과에서만 뒤로가기 버튼 보이기 */}
      {findResultsPage ? (
        <IoChevronBackOutline size="22" color="#353D4A" onClick={handleBack} style={{ marginLeft: '-8px' }} />
      ) : null}
      <div>
        <span className="search">
          <BiSearch />
        </span>
        <input type="text" placeholder="필요한 상품을 찾아보세요" value={inputValue} onChange={handleInputChange} />
        {/* 검색바에 글자 있을 때만 삭제버튼 노출 */}
        {inputValue !== '' ? (
          <button type="button" className="delete" onClick={handleDeleteBtn}>
            <TiDelete />
          </button>
        ) : null}
      </div>
    </Container>
  );
};
  • submit
    • 검색바에서 enter를 눌렀을 때 검색바에 적혀있는 것이 있다면, (1)검색결과로 이동하고 (2)자동저장 상태 값에 따라 서버의 최근검색어 목록에 검색어를 추가하거나 추가하지 않는다.
  • 뒤로가기
    • useLocation을 사용해 pathname으로 조건을 설정하여, 검색결과 페이지면 뒤로가기 아이콘을 보여주고 아니라면 아이콘을 보여주지 않는다.
  • 검색어 삭제
    • input에 onChange 이벤트를 걸어 값이 변화할 때마다 setState 하여 값의 상태를 관리한다. 이를 통해 값이 있으면 키워드 삭제 버튼을 보여주고, 값이 없으면 보여주지 않는다. 삭제 버튼을 클릭하면 input 의 value를 ''으로 변경한다.
  • 검색바에 검색어 표시
    • useParams를 사용해 라우터에서 :keyword 부분이 있으면, 검색바에 그 값을 출력하도록 했다. 검색결과 페이지의 라우터는 /search/:keyword로 설정했으므로, 검색결과 페이지에서만 검색어가 보인다.
      👉구현 영상
profile
FE Developer | 차근차근

0개의 댓글