ToyProject(더다주) 무한스크롤 & sort

노영완·2023년 7월 29일
0

ToyProject(더다주)

목록 보기
7/13

추천상품은 sort기능이 들어간 무한스크롤을 구현

1. 무한스크롤을 통해 상품이 나오게 끔.

2. 신상품 상품명 가격 낮은 높은 순으로 데이터가 나오게끔.

3. 위에 두가지를 유지한채로 상품이 보여주게 끔.

첫 생각

서버 : pagination은 offset 방식을 사용 query는 page limit sort를 써 데이터가 나오게끔. sort의 기준은 조건문을 통해 각 조건에 맞는 데이터가 나오게끔 구현.

클라이언트 : Intersection Observer를 사용해 인피니트 스크롤을 구현. sort 부분은 select태그에 OnChange를 걸어 value값을 얻어와 url이 바뀌게끔 진행

Server Code

productRouter.get("/recommend", async (req, res) => {
  try {
    const { page = 1, limit = 8, sort } = req.query;
    if (sort === "높은가격") {
      const [recommendProducts, count] = await Promise.all([
        await RecommendProduct.find({})
          .sort({ price: -1 })
          .limit(limit)
          .skip((page - 1) * limit),
        await RecommendProduct.count(),
      ]);
      return res.json({
        recommendProducts,
        count,
      });
    }
    if (sort === "낮은가격") {
      const [recommendProducts, count] = await Promise.all([
        await RecommendProduct.find({})
          .sort({ price: 1 })
          .limit(limit)
          .skip((page - 1) * limit),
        await RecommendProduct.count(),
      ]);
      return res.json({
        recommendProducts,
        count,
      });
    }
    if (sort === "상품명") {
      const [recommendProducts, count] = await Promise.all([
        await RecommendProduct.find({})
          .sort({ name: 1 })
          .limit(limit)
          .skip((page - 1) * limit),
        await RecommendProduct.count(),
      ]);
      return res.json({
        recommendProducts,
        count,
      });
    }
    if (sort === "null" || sort === "신상품") {
      const [recommendProducts, count] = await Promise.all([
        await RecommendProduct.find({})
          .sort({ _id: 1 })
          .limit(limit)
          .skip((page - 1) * limit),
        await RecommendProduct.count(),
      ]);
      return res.json({
        recommendProducts,
        count,
      });
    }
  } catch (e) {
    return res.status(500).json({ err: e.message });
  }
});

page에 defalut 값은 1 limit에 defalut 값은 8로 해주었고, 클라이언트에서 넘어오는 sort의 종류에 따라 정렬을 다르게 하여 데이터를 전달해 주었다.

클라이언트

처음 무한스크롤을 구현하기로 마음먹었을때는 react-infinite-scroll-component 라이브러리를 사용해 구현할려고 했다. 아무래도 react-query를 공부 할 때 사용해본 경험이 있어서 익숙한거에 이끌렸다. 하지만, 학습의 목적으로 만든 프로젝트인 만큼 새로운거를 접해보고 해봐야겠다고 생각했다. 그래서 Intersection Observer를 사용하기로 했다.

문제


1. useLocation으로 불러올시 key값이 중복되어 들어와 콘솔에 에러가 뜨는 문제
2. useLocation으로 불러오지 못하는 문제 때문에 select에서의 문제

문제해결

앞선 문제는 navigate로 url이 넘어가는 시점에서의 데이터가 중복되는 현상이었다. 나는 server 쪽에서 문제를 해결하기로 생각하였고, 기존 offset방식이 아닌 cursor 방식을 사용해 상품의 id값을 넘겨 중복되는 현상을 방지하고자 하였다. 하지만, 내가 생각한 해결방식은 해결방안이 되지 못하였다. 그대로 key값이 중복되었다.

Client Code

  const [recommendData, setRecommendData] = useState<ProductType[]>([]);
  const [totalData, setTotalData] = useState();
  const [hasNextPage, setHasNextPage] = useState<boolean>(true);
  const page = useRef<number>(1);
  const observerTargetEl = useRef<HTMLDivElement>(null);
  const location = useLocation();
  const navigate = useNavigate();
  const urlSearchParam = new URLSearchParams(location.search);
  const currentPageString = urlSearchParam.get("sort");
  const infiniteProduct = useCallback(async () => {
    await recommendProduct(
      `?page=${page.current}&limit=8&sort=${currentPageString}`
    ).then((data) => {
      setRecommendData((prevData) => [...prevData, ...data.recommendProducts]);
      setTotalData(data.count);
      setHasNextPage(data.recommendProducts.length === 8);
      if (data.recommendProducts.length) {
        page.current += 1;
      }
    });
  }, []);
  useEffect(() => {
    if (!observerTargetEl.current || !hasNextPage) return;
    const io = new IntersectionObserver((entries, observer) => {
      if (entries[0].isIntersecting) {
        infiniteProduct();
      }
    });
    io.observe(observerTargetEl.current);
    return () => {
      io.disconnect();
    };
  }, [infiniteProduct, hasNextPage]);
  const onChangeSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
    navigate(`/product?page=1&limit=8&sort=${e.currentTarget.value}`);
    window.location.reload();
  };
  const infiniteProduct = useCallback(async () => {
    await recommendProduct(
      `?page=${page.current}&limit=8&sort=${currentPageString}`
    ).then((data) => {
      setRecommendData((prevData) => [...prevData, ...data.recommendProducts]);
      setTotalData(data.count);
      setHasNextPage(data.recommendProducts.length === 8);
      if (data.recommendProducts.length) {
        page.current += 1;
      }
    });
  }, []);

hasNextPage로 (data가 8개 들어오면) true가 되게끔 핸들링 또한, true이면 page의 값이 1이 늘어나게끔 하였다. 그리고 page는 useState가 아닌 useRef를 사용하였는데 useState를 page의 변화에따라 리랜더링이 일어나고 리랜더링은 서버에 무수한 요청이 될 수도 있어 useRef를 사용

  useEffect(() => {
    if (!observerTargetEl.current || !hasNextPage) return;
    const io = new IntersectionObserver((entries, observer) => {
      if (entries[0].isIntersecting) {
        infiniteProduct();
      }
    });
    io.observe(observerTargetEl.current);
    return () => {
      io.disconnect();
    };
  }, [infiniteProduct, hasNextPage]);

앞선 코드에 첫번째줄은 관찰하지 못하거나 혹은 데이터가 끝나는 시점에서의 즉, 마지막 데이터가 오는 그 다음시점에서의 코드가 실행되지 않기 위한 코드 new IntersectionObserver로 IntersectionObserver를 생성 observe()를 사용해 타켓 엘리먼트 지정, entries는 배열이며, 현재 IntersectionObserver를 통해 관찰할 부분은 하나이므로 index로 접근하였으며 isIntersecting을 통해 true일 때() infiniteProduct() 실행. true를 반환하지 못할시 즉, 앞선 코드를 실행하지 못할때는 disconnenct를 통해 관찰 중지.

  const onChangeSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
    navigate(`/product?page=1&limit=8&sort=${e.currentTarget.value}`);
    window.location.reload();
  };
// select tag 관련 코드
 {currentPageString === null && (
            <>
              <option value="신상품">신상품</option>
              <option value="상품명">상품명</option>
              <option value="낮은가격">낮은가격</option>
              <option value="높은가격">높은가격</option>
            </>
          )}
          {currentPageString === "신상품" && (
            <>
              <option value="신상품">신상품</option>
              <option value="상품명">상품명</option>
              <option value="낮은가격">낮은가격</option>
              <option value="높은가격">높은가격</option>
            </>
          )}
          {currentPageString === "상품명" && (
            <>
              <option value="상품명">상품명</option>
              <option value="신상품">신상품</option>
              <option value="낮은가격">낮은가격</option>
              <option value="높은가격">높은가격</option>
            </>
          )}
          {currentPageString === "낮은가격" && (
            <>
              <option value="낮은가격">낮은가격</option>
              <option value="상품명">상품명</option>
              <option value="신상품">신상품</option>
              <option value="높은가격">높은가격</option>
            </>
          )}
          {currentPageString === "높은가격" && (
            <>
              <option value="높은가격">높은가격</option>
              <option value="상품명">상품명</option>
              <option value="신상품">신상품</option>
              <option value="낮은가격">낮은가격</option>
            </>
          )}

useLocation으로 url에 정보를 불러오면 key가 중복되어 에러가 나 클라이언트 측에서 그에 맞게 해결한 select 코드이며, useState로 관리해 sort를 구현하는 것이 아닌 navigate를 보내 sort query 부분을 가지고 오는 이유에는 state로 관리시 첫 페이지에 데이터가 아닌 다음 페이지에서의 sort에 맞는 데이터를 가져오는 문제 때문에 navigate로 보낸 후 url에 값을 가져오고 리다이렉트를해 새롭게 데이터를 받는 방식을 선택했다. 이런 방식을 선택한데 문제도 따랏는데 select sort 부분을 선택하는 부분에서 신상품으로 계속 고정이 되는 문제가 있어 사용자에게 혼란을 줄 수 있다고 생각했다. 그래서 url에 querysort 값에 따라 보여지는게 다르게끔 하였다.

아쉬운 점

useLocation으로 url을 핸들링 하지 못해 좋지 못한 코드라고 생각하고 select 또한 억지로 만든 느낌 또한 있어 만족하지 못한다.
추후 다시 수정해야 할 문제이다. 또한 스켈레톤 UI 혹은 로딩 중 이라는 컴포넌트를 랜더링하지 못했다는 점에서도 아쉽다. 이런 부분은 사용자에게 UX적인 부분을 채울 수 있다고 생각하기 때문이다. 추후 수정할 문제이다.

0개의 댓글