DEVELOP
article thumbnail

React에서 페이지네이션과 sorting(최신순 / 별점높은순) 을 구현하고자 한다.

현재 리뷰리스트를 뿌려주는 페이지를 구현 중이다.

백엔드에서 sortType과 pageNum에 따라 데이터를 뿌려주며, 전체 데이터 수도 전달받는 다는 가정 하에 진행한다.

URL 주소의 파라미터

http://localhost:3000/review?sort=latest&page=1
위와 같은 방식으로 sort 방식과 page를 파라미터로부터 받아와서 맞는 데이터를 뿌려줄 것이다.

// src/pages/ReviewListPage/ReviewListPage.jsx

/* 리뷰페이지 url로부터 params 값 가져오기 */
  const location = useLocation();
  const sort = new URLSearchParams(location.search).get('sort') || 'latest'; // 정렬기준
  const page = new URLSearchParams(location.search).get('page') || 1; // 페이지
  • 정렬기준이 최신순이면 latest, 별점높은 순이면 popular 로 주소의 파라미터를 설정한다.
  • page는 page 파라미터에 숫자를 설정한다.
  • 각각 기본값은 latest, 1 이다.

Get 요청하기

백엔드에서 정의한 url주소와 규칙에 맞게 sortType과 page를 파라미터로 한 커스텀 훅을 작성한다.

// src/hooks/getReviews.js

import { instance } from '.';
import { useQuery } from 'react-query';
import { showAlert } from '../util/showAlert';

const getReviews = async (sortType = 'latest', page = 1) => {
  try {
    const response = await instance.get(
      `/reviews?sortBy=${sortType}&page=${page}`,
    );

    if (response?.data?.data) {
      return response.data.data;
    }
  } catch (error) {
    throw error;
  }
};

export function useGetReviews(sortType, page) {
  return useQuery(
    ['getReviews', sortType, page],
    () => getReviews(sortType, page),
    {
      keepPreviousData: true,
      retry: false,
      onError: (error) => {
        showAlert('', '리뷰 목록을 불러올 수 없습니다.', 'error', () => {
          window.history.back();
        });
      },
    },
  );
}
  • 리뷰페이지에서 해당 hook을 불러온다.
  • 리뷰데이터가 변경되면 list state를 업데이트한다.
// src/pages/ReviewListPage/ReviewListPage.jsx

/* 정렬기준, 페이지에따라 리뷰 리스트 불러오기 */
const { data: reviews, isLoading } = useGetReviews(sort, page);

const [list, setList] = useState([]);

useEffect(() => {
    if (reviews) {
      setList(reviews.reviews);
    }
  }, [reviews]);

UI 구현

정렬기준 선택은 아래 처럼 선택된 곳에 노란색으로 배경이 칠해지는 원과 라벨이 보이게 스타일링하고,

페이지네이션은 가장 하단에 페이지번호버튼과 다음 페이지 버튼, 다음범위페이지 버튼을 두었다.

선택된 페이지에는 노란색으로 원 테두리를 그린다.

  • 원래는 한 화면에 10페이지까지 보이게 할 것인데, 현재 데이터가 많지 않기 때문에 임의로 4페이지를 한 화면에 보이도록 한다.

정렬기준 UI 구현

// src/components/ReviewListSort/ReviewListSort.jsx

import { Container } from './styles';

const ReviewListSort = ({ handleSorting }) => {
  return (
    <Container>
      <label>
        <input
          type="radio"
                    name="sortType"
          value="latest"
          defaultChecked
        />
        <span>최신순</span>
      </label>
      <label>
        <input
          type="radio"
          name="sortType"
          value="popular"
        />
        <span>별점높은순</span>
      </label>
    </Container>
  );
};
export default ReviewListSort;

// src/components/ReviewListSort/styles.jsx

import styled from 'styled-components';

export const Container = styled.div`
  display: flex;
  margin: 20px;
  font-size: 16px;

  span {
    margin-right: 10px;
  }

  label {
    display: flex;
    align-items: center;
  }

  input[type='radio'] {
    margin: 0 5px;
    appearance: none;
    width: 14px;
    height: 14px;
    border: 2px solid var(--main-yellow-color);
    border-radius: 50%;
    cursor: pointer;
  }

  input[type='radio']:checked {
    background-color: var(--main-yellow-color);
  }
`;
  • 체크된 라디오버튼에는 배경을 노란색으로 변경한다.

페이지네이션 UI 구현

// src/components/ReviewPagination/ReviewPagination.jsx

import { useState } from 'react';
import { Button, Container } from './styles';

const ReviewPagintaion = ({ currentPage, navigatePage, totalNum }) => {
  const pagePerScreen = 4; // 한 화면에 보이는 페이지 번호 갯수
  const reviewsPerPage = 10; // 한 페이지 당 보일 리뷰 갯수

  const pageNum = parseInt(totalNum / reviewsPerPage) + 1; // 총 페이지 갯수 (전체/한 페이지 당 리뷰 수)

  // 현재 보여지는 페이지 범위 상태
  const [visiblePageStart, setVisiblePageStart] = useState(1);
  const visibledPageEnd = visiblePageStart + pagePerScreen - 1;

  return (
    <Container>
        <Button>
          &lt;&lt;
        </Button>
        <Button>&lt;</Button>

      {Array.fill(pageNum)
        .map((_, i) => (
          <Button
            key={i+1}
            aria-current={
              currentPage === i+1 ? 'page' : undefined
            }
          >
            {i+1}
          </Button>
        ))}
        <Button>&gt;</Button>
        <Button
        >
          &gt;&gt;
        </Button>
      )}
    </Container>
  );
};

export default ReviewPagintaion;

// src/components/ReviewPagination/styles.jsx

import styled from 'styled-components';

export const Container = styled.div`
  gap: 8px;
  display: flex;
  padding: 50px 0;
  margin: 0 auto;
  @media screen and (max-width: 767px) {
    gap: 5px;
    padding: 30px 0;
  }
`;

export const Button = styled.button`
  display: inline-block;
  width: 30px;
  height: 30px;
  background-color: transparent;
  border-radius: 50%;
  font-size: 20px;
  color: black;
  text-align: center;

    &[aria-current] {
    border: solid 3px var(--main-yellow-color);
  }

  &:hover {
    background-color: var(--main-yellow-color);
  }
`;

리뷰페이지 UI 구현

// src/pages/ReviewListPage/ReviewListPage.jsx

// ...

return isLoading ? (
    <Loading />
  ) : (
    <Container>
      <Title>리뷰 모아보기</Title>
      <ReviewListContainer>
        <ReviewListSort/>
        {list &&
          list.map((item) => <ReviewItem key={item._id} id={item._id} />)}
      </ReviewListContainer>
      <ReviewPagintaion
        currentPage={parseInt(page)}
        totalNum={reviews?.totalReviews}
      />
    </Container>
  );
  • reviewItem 컴포넌트 및 Title 컴포넌트에 대한 설명은 생략한다.

sorting 구현하기

비교적 간단한 sorting을 먼저 구현해보자.

나의 경우에 기준은 2가지로, 최신순 / 별점높은순 이 있다.

  • 정렬기준이 변경될 때마다 현재 주소창의 파라미터를 변경하고 화면 최상단으로 스크롤을 이동하는 함수 handleSorting 을 정의하고,
  • ReviewListSort 컴포넌트에 넘겨준다.
// src/pages/ReviewListPage/ReviewListPage.jsx

/* 정렬기준 변경 */
  const handleSorting = (sortType) => {
    navigate(ROUTE.REVIEW_LIST_PAGE.link + `?sort=${sortType}&page=${page}`);

    window.scroll({ top: 0, behavior: 'smooth' });
  };

// ...

<ReviewListSort handleSorting={handleSorting} />
  • ReviewListSort 컴포넌트에는 라디오 버튼의 값이 변경될 때마다 onChange 함수로서 handleSorting 함수에 해당 정렬 기준 값을 넘겨준다.
// src/components/ReviewListSort/ReviewListSort.jsx

const ReviewListSort = ({ handleSorting }) => {
  return (
    <Container>
      <label>
        <input
                        ...
          onChange={() => handleSorting('latest')}
        />
        <span>최신순</span>
      </label>
      <label>
        <input
          ...
          onChange={() => handleSorting('popular')}
        />
        <span>별점높은순</span>
      </label>

그러면 sortType이 바뀔 때마다 주소가 바뀌어 GET 요청을 다시 보내고, 리뷰 아이템들을 업데이트 하게 된다.

페이지네이션 구현하기

페이지 변경 함수 넘겨주기

먼저, 페이지 값이 바뀌면 주소 값을 바꿔 이동하고, 스크롤을 최상단으로 이동시키는 함수 navigatePage 를 정의하고, ReviewPagination 컴포넌트에 넘겨준다.

// src/pages/ReviewListPage/ReviewListPage.jsx

/* 페이지 변경 */
  const navigatePage = (newPage) => {
    navigate(ROUTE.REVIEW_LIST_PAGE.link + `?sort=${sort}&page=${newPage}`);

    window.scroll({ top: 0, behavior: 'smooth' });
  };

// ...

<ReviewPagintaion
        currentPage={parseInt(page)}
        navigatePage={navigatePage}
        totalNum={reviews?.totalReviews}
      />

페이지 변경 및 범위 설정 함수 정의

ReviewPaination 컴포넌트에서는 이동할 페이지를 파라미터로 받아서 이동하고, 시작/끝의 범위를 조절하는 함수 setPage 를 정의한다.

  • 이동하고자하는 페이지가 현재 끝페이지보다 크면, 범위 밖으로 이동하는 것이므로 범위를 변경하기 위해서 visiblePageStart 값을 현재 값에 한 범위에 나오는 최대 페이지 값인 pagePerScreen을 더한값으로 설정한다.
  • 이동하고자하는 페이지가 현재 시작페이지보다 작으면, 현재 범위보다 이전 범위로 이동하는 것이므로, visiblePageStart 값을 현재 값에 pagePerScreen 을 뺀 값으로 설정한다.
  • 이후 이동하고자하는 페이지로 이동하도록 페이지 값을 넘겨준다.
// src/components/ReviewPagination/ReviewPagination.jsx

// ...

// 이동할 페이지를 파라미터로 받아 이동하며, 시작/끝 범위 조절
  const setPage = (targetPage) => {
    if (visibledPageEnd < targetPage) {
      setVisiblePageStart(visiblePageStart + pagePerScreen);
    }
    if (visiblePageStart > targetPage) {
      setVisiblePageStart(visiblePageStart - pagePerScreen);
    }
    navigatePage(targetPage);
  };

이전/다음 화살표 버튼에 조건 설정

  • 이전 범위로 이동하는 << 버튼은 첫번째 범위 (1~4) 가 아닐때만 나와야하므로, visiblePageStart가 1보다 클때만 나오도록하고, setPage 함수에 현재 페이지 값에 pagePerScreen 을 빼준 값을 넘겨준다.
  • 이전 페이지로 이동하는 < 버튼은 첫번째 페이지가 아닐때만 나와야하므로, 현재페이지(currentPage)가 1보다 클때만 나오도록하고, setPage 함수에 현재 페이지 값에 1을 빼준 값을 넘겨준다.

      {visiblePageStart > 1 && (
        <Button onClick={() => setPage(currentPage - pagePerScreen)}>
          &lt;&lt;
        </Button>
      )}

      {currentPage > 1 && (
        <Button onClick={() => setPage(currentPage - 1)}>&lt;</Button>
      )}
  • 다음 페이지로 이동하는 > 버튼은 마지막페이지가 아닐때만 나와야하므로, 현재페이지가 pageNum가 아닐때만 나오도록 하고, setPage 함수에 현재 페이지 값에 1을 더한 값을 넘겨준다.
  • 다음 범위로 이동하는 >> 버튼은 마지막범위가 아닐때만 나와야하므로, visibledPageEnd가 전체 페이지 수 (pageNum)보다 작을 때만 나오도록하고, setPage 함수에 현재 페이지 값에 pagePerScreen 을 더한 값을 넘겨준다.
    • 단, 만약 전체 페이지가 10까지 있고, 7에서 >> 버튼을 클릭하면 11로 이동하게 되는데, 전체 페이지보다 큰 값으로 이동할 수 없으므로 그럴 경우에는 pageNum값을 파라미터로 넘겨준다.
            {currentPage !== pageNum && (
        <Button onClick={() => setPage(currentPage + 1)}>&gt;</Button>
      )}
      {visibledPageEnd < pageNum && (
        <Button
          onClick={() =>
            setPage(
              currentPage + pagePerScreen <= pageNum
                ? currentPage + pagePerScreen
                : pageNum,
            )
          }
        >
          &gt;&gt;
        </Button>
      )}

 

결과

 

깃허브 주소를 통해 전체 소스코드를 확인할 수 있다. 

https://github.com/elice-final-team6/MongMongVillage-FE/blob/dev/src/pages/ReviewListPage/ReviewListPage.jsx

 

profile

DEVELOP

@JUNGY00N