ToyProject(더다주) search

노영완·2023년 7월 20일
0

ToyProject(더다주)

목록 보기
1/13
post-thumbnail

시연영상

이번 프로젝트를 진행하면서 search 기능을 넣기로 했다.

FE: React Redux-toolkit styled-components

BE: node mongodb mongoose

1. search 모달은 어디서든 서치 아이콘을 누르면 서치 모달이 나온다

2. 위에 아래로 내려오는 자연스러운 모달 구현.

3. search 모달에서 검색한 내용을 state에 담아 state에 내용에 따른 product 랜더링 그리고 state는 계속남아 어떤 내용을 검색한건지 사용자에게 알려주게끔.

첫 생각

  1. Search라는 파일을 만든 후 그 파일에서 modal과 searchProudctList를 관리해준다.
  2. 그럼 자연스럽게 useState로 관리한 state는 props로 내려 관리가 된다. 이게 첫 search 파일 구성이었다 이렇게 되어야 product 파일들을 한번에 모아 보기 편하다고 생각했다.
const [toggle, setToggle] = useState(false)
// 이 state로 props를 넘겨 modal 유무를 관리해 주었다.

또한 Router에도 path를 지정해주었다.
그 결과, Router에 path만 지정해 준 url에서만 modal이 나타났다.

스스로 피드백 후 생각

  1. Search modal 과 SearchProduct는 따로 생각해주어야 한다. 나는 Product만 모이게끔 파일을 정리하고 싶었기 때문이며, SearchModal은 어디서든 나와야 하기 때문이다.
  2. SearchModal을 nav와 footer처럼 공통 컴포넌트로 생각하자 그럼 어디서든 핸들링이 가능할 것이다.
  3. searchmodal에 대한 핸들링 스테이트는 redux로 관리해 전역으로 뿌리자 그러면 따로 분리가 가능하다. 이게 내가 다시 생각해낸 생각이다.
// Router
  <BrowserRouter>
      <Nav />
      <Search />
      <Routes>
        <Route path="/" element={<Main />} />
        <Route path="/login" element={<AuthLogin />} />
        <Route path="/create" element={<AuthCreate />} />
        <Route path="/product" element={<Product />} />
        <Route path="/product/:pageId" element={<Detail />} />
        <Route path="/mypage" element={<MyPage />} />
        <Route path="/cart" element={<Basket />} />
        <Route path="/wishlist" element={<WishList />} />
      </Routes>
      <Footer />
    </BrowserRouter>
// redux 관련 코드
// redux-toolkit을 사용했다.
const initialState = {
  toggleSearch: false,
  productSearch: "",
};
export const productSlice = createSlice({
  name: "productSlice",
  initialState,
  reducers: {
    setToggleSearch: (
      state,
      action: PayloadAction<{ toggleSearch: boolean }>
    ) => {
      state.toggleSearch = action.payload.toggleSearch;
    },
    setProductSearch: (
      state,
      action: PayloadAction<{ productSearch: string }>
    ) => {
      state.productSearch = action.payload.productSearch;
    },
  },
export const { setToggleSearch, setProductSearch } =
  productSlice.actions;

일부분만 가져온 코드이다. toggle에 관련된 state, search를 한 검색어에 대한 state에 관련한 코드이다. 이 state들은 전역으로 뿌려 관리해 줄 것이다.

// nav에 searchmodal에 대한 아이콘 코드
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../store";
import { setToggleSearch } from "../../reducers/productSlice";
 const navigateSearch = () => {
    dispatch(setToggleSearch({ toggleSearch: true }));
  };

dispath를 통해 state에 값을 true로 변경 modal이 on이 되게끔 설정.

//searchModal에 관련한 코드  
const [search, setSearch] = useState("");
  const toggleSearch = useSelector(
    ({ product }: RootState) => product.toggleSearch
  );
  const dispatch: AppDispatch = useDispatch();
  const navigate = useNavigate();
  const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    setSearch(value);
  };
  const onClickClose = () => {
    dispatch(setToggleSearch({ toggleSearch: false }));
  };
  const onSubmitSearch = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    await Promise.all([
      await dispatch(setProductSearch({ productSearch: search })),
      await dispatch(setToggleSearch({ toggleSearch: false })),
    ]);
    navigate("searchProdcut에 맞는 URL");
    setSearch("");
  };

useSelector로 toggleSearch인 전역 변수를 가져와 핸들링 해준다. 그리고 dispatch로 다시 productSearch에 관련한 state를 관리 productList에서 useSelector로 가져온다.

첫 생각

  1. toggleSearch 불리언 state에 따른 조건부 랜더링으로 모달이 생겼다 안생겼다로 구현하자.
  2. keyframes를 이용해 animation 구현.
    그, 결과 false일때는 animation 무효화 이유는 애초에 조건부 랜더링으로 false가 되어버렸으니 아예 keyframes는 실행이 안된다.

다시 생각

  1. 불리언 스테이트로 조건부 랜더링을 하면 안된다.
  2. 굳이 구간을 나누어야 할만한 animation이 아닌데 keyframes를 써야할까?
import styled, { css } from "styled-components";
return(
 <form onSubmit={onSubmitSearch}>
        <SearchComponent visible={toggleSearch}>
          <SearchTitle>SEARCH</SearchTitle>
          <SearchFieldSet>
            <SearchInput value={search} onChange={onChangeSearch} type="text" />
            <SerchIcon />
          </SearchFieldSet>
          <CloseIcon onClick={onClickClose} />
        </SearchComponent>
      </form>
<BackGround visible={toggleSearch} />
)
const SearchComponent = styled.div<{ visible: boolean }>`
  z-index: 1001;
  ${({ theme }) => theme.positionMixIn("fixed", 0, 0)};
  left: 0;
  height: 389px;
  opacity: 0;
  visibility: hidden;
  transform: translate3d(0, -100%, 0);
  transition: opacity 0.5s ease, transform 0.5s ease, visibility 0s linear 0.5s;
  background: #fff;
  ${(props) =>
    props.visible &&
    css`
      opacity: 1;
      visibility: visible;
      transition: opacity 0.5s ease, transform 0.5s ease,
        visibility 0s linear 0s;
      transform: translateZ(0);
    `}
`;

조건부 랜더링을 스타일드 컴포넌트에서 사용한다. 그리고 visible 속성을 이용해 나오게끔 안나오게끔을 구현 조건부 랜더링에 따른 다른 속성으로 위에서 아래로 아래에서 위로에 animation 구현.

첫 생각

1. 공부해보니 index를 이용해 db의 읽기 성능을 올리고 $text를 이용해 내가 원하는 값을 search 하게끔 가능하겠다.

2. 지금 새상품 추천상품 이렇게 나누어져있는데 새상품 추천상품 할 것 없이 공통된 검색어가 나오면 상품이 나와야 하는데 이걸 어떻게 해결하지

// model
const { Schema, model } = require("mongoose");
const RecommendProductSchema = new Schema(
  {
    src: { type: String, required: true },
    name: { type: String, index: true, required: true },
    price: { type: Number, index: true, required: true },
  },
  { timestamps: true }
);
RecommendProductSchema.index({ name: "text" });
const RecommendProduct = model("recommendproduct", RecommendProductSchema);
module.exports = { RecommendProduct };

$text에 관련한 index를 생성한다. RecommendProductSchema.index({ name: "text" }) 라는 코드를 통해 $text는 name이 된다. 즉 , name에 관련으로 search를 할 수 있다.

productRouter.get("/search", async (req, res) => {
  try {
    const { search, page = 1, limit = 4 } = req.query;
    const [
      searchProudctFirst,
      searchProductSecond,
      totalProductOne,
      totalProductTwo,
    ] = await Promise.all([
      await RecommendProduct.find({ $text: { $search: search } })
        .sort({
          _id: 1,
        })
        .limit(limit)
        .skip((page - 1) * limit),
      await NewProduct.find({ $text: { $search: search } })
        .sort({
          _id: 1,
        })
        .limit(limit)
        .skip((page - 1) * limit),
      await RecommendProduct.count({ $text: { $search: search } }),
      await NewProduct.count({ $text: { $search: search } }),
    ]);
    const searchProduct = [...searchProudctFirst, ...searchProductSecond];
    const totalProduct = totalProductOne + totalProductTwo;
    return res.json({
      searchProduct,
      totalProduct,
    });
  } catch (e) {
    res.status(500).json({ message: e.message });
  }
});

위의 코드는 인피니트 스크롤도 구현해야 하기 때문에 query에 page와 limit이 있다 search에 기능에는 관련이 없다. find({ $text: { $search: search } ) find로 $text에 걸려있는 index에서 search를 할 것이다. 어떤 것을 search 하냐 query에 들어온 search 값에 따른 값을 서치할 것이고 나는 한번에 모든 상품들을 나열해야 했기때문에 배열에 이터럴을 통해 새 배열을 만들어 client에 보내줬다.

1개의 댓글

comment-user-thumbnail
2023년 7월 20일

정말 좋은 글 감사합니다!

답글 달기