재사용 가능한 Pagination 컴포넌트 만들기

이주희·2022년 4월 2일
2

Libraries

목록 보기
7/21

연관 내용
[무한 스크롤 적용하기]

1. Query를 수정한다.

page를 input값으로 넣어야 다음 페이지가 조회된다.
(안 넣으면 같은 페이지만 계속 불러옴..)

  query fetchBoards($page: Int, $search: String) {
    fetchBoards(page: $page, search: $search) {
      _id
      writer
      title
      likeCount
      images
      createdAt
    }
  }
`;

2. refetch를 추가한다.

const { data, refetch } = useQuery(FETCH_BOARDS);
기존에 쓰던 refetchQueries는 mutation 이후에 작성했다.
지금 사용하려는 useQuery의 refetch는 data 옆에 refetch를 작성해서 사용한다.

data는 state와 마찬가지로, 값이 바뀌면 화면이 리렌더 된다.
이 원리를 토대로 refetch를 해서 data가 바뀌면 화면이 다시 그려지게 된다.

참고: 이전에 쓰던 refetchQueries👇🏻

const onClickLike = async () => {
    try {
      likeBoard({
        variables: { boardId: String(router.query.boardId) },
        refetchQueries: [
          { query: FETCH_BOARD, variables: { boardId: router.query.boardId } },
        ],
      });
    } catch (error) {
      if (error instanceof Error) console.log(error.message);
    }
  };

3. 페이지를 누르면 refetch를 실행하는 함수를 만든다.

refetch(Number{page: e.target.id})
refetch를 사용할 때는 안에 객체 형태로 variables를 넣어주면 된다.

4. page 수를 구한다.

  • 전체 data 수를 구해서 (백엔드에서 api를 통해 제공한다.)
    한 화면에 보여줄 data 수로 나눈 값을 마지막 페이지 번호로 저장한다. --> pagination으로 보냄

  • startPage는 lastPage보다 같거나 작다.

5. page 번호 배열을 만든다.

  • 기준 페이지 startPage를 만들고 초기값으로 1을 넣는다.

  • 한 페이지에 보여줄 data의 수를 길이로 갖는 배열을 만들고 내용을 (아무거나) 채운다.
    (함수를 실행시키는 횟수를 위한 길이만 사용할 거라서 내용은 뭐든 상관 없다.)

  • map의 index를 사용해서 1씩 증가하는 페이지 번호를 만든다.

  • startPage + indexlastPage를 넘어가면 data가 없는데도 페이지 번호가 생성되기 때문에,
    map을 돌리기 전에 filter로 걸러준다.

  • page 번호를 누르면 3에서 만든 onClickPage 함수가 실행되고,
    id에 넣어둔 startPage+index로 refetch가 이루어진다.

6. 이전/다음 버튼을 추가한다.

  • 이전 버튼을 누르면 startPage-10,
    다음 버튼을 누르면 startPage+10으로 startPage를 변경한다.

  • 변경할 startPage로 refetch를 실행한다.

  • 최초 페이지와 마지막 페이지일 때는 버튼이 눌리지 않도록 if문을 작성해서 return시킨다.

(setIsPrev/NextActive와 setCurrent는 현재 page에 따라 style을 적용하기 위해 추가했다.)


FreeBoard에 적용한 코드

Boards.container.tsx

/* FETCH_BOARDS */
  const { data, refetch } = useQuery(FETCH_BOARDS); // data는 state와 동일한 역할을 한다. 값이 바뀌면 리렌더된다.

...

/* Pagination에 쓸 데이터 */
  const { data: dataBoardsCount } = useQuery(FETCH_BOARDS_COUNT, {
    variables: { search },
  });
  const lastPage = Math.ceil(dataBoardsCount?.fetchBoardsCount / 10);
  const [current, setCurrent] = useState<number>(1); // 게시글 번호와 현재 페이지 표시 style을 줄 때 사용
		
...
        
 return (
    <BoardsUI
      lastPage={lastPage}
      current={current}
      setCurrent={setCurrent}
		...
    />
  );
}

Boards.presenter.tsx

{/* 게시글 번호 */}
  <S.TD style={{ width: "100px" }}>
    {props.current * 10 + index - 9}
  </S.TD>

...

 <Pagination
      refetch={props.refetch}
      lastPage={props.lastPage}
      current={props.current}
      setCurrent={props.setCurrent}
      />

Pagination.tsx

import { useState } from "react";
import { IPaginationProps } from "../../units/board/list/Boards.types";
import * as S from "./Pagination.style";

export default function Pagination(props: IPaginationProps) {
  const [startPage, setStartPage] = useState(1);
  const [isPrevActive, setIsPrevActive] = useState(false);
  const [isNextActive, setIsNextActive] = useState(true);

  const onClickPage = (e: any) => {
    // Query 확인하자! (variables에 page 추가)
    props.refetch({ page: Number(e.target.id) });
    props.setCurrent(Number(e.target.id));
    console.log("onClickPage");
  };

  const onClickPrevPage = () => {
    if (startPage === 1) {
      // 첫 페이지 묶음이면 실행ㄴㄴ
      setIsPrevActive(false);
      return;
    }
    if (startPage === 11) {
      setIsPrevActive(false);
    }
    setIsNextActive(true);
    setStartPage((prev) => prev - 10); // 이전 페이지 묶음으로 이동
    props.refetch({ page: startPage - 1 }); // prev 버튼 눌렀을 때 이전 10개의 페이지 중 가장 마지막이 열리도록 변경
    props.setCurrent(startPage - 1);
  };

  const onClickNextPage = () => {
    if (props.lastPage < startPage + 10) {
      // 마지막 페이지 묶음이면 실행ㄴㄴ
      setIsNextActive(false);
      return;
    }
    if (props.lastPage < startPage + 20) {
      setIsNextActive(false);
    }
    setIsPrevActive(true);
    setStartPage((prev) => prev + 10); // 다음 페이지 묶음으로 이동
    props.refetch({ page: startPage + 10 });
    props.setCurrent(startPage + 10);
  };

  return (
    <S.Wrapper>
      <S.Button
        disabled={!isPrevActive}
        onClick={onClickPrevPage}
        isActive={isPrevActive}
      >
        &lt;
      </S.Button>
      <S.PageNumberWrapper>
        {new Array(10) // 한 페이지에 보여줄 data 수
          .fill(1) // 아무거나 채움
          // 마지막 페이지 묶음에서 데이터가 없는 번호는 노출하지 않도록 filter
          .filter((_, index) => index + startPage <= props.lastPage) // el을 사용하지 않아서 _로 씀
          .map((_, index) => (
            <S.PageNumber
              onClick={onClickPage}
              id={String(index + startPage)}
              key={index + startPage}
              isCurrent={props.current === index + startPage}
            >
              {index + startPage}
            </S.PageNumber>
          ))}
      </S.PageNumberWrapper>
      <S.Button
        disabled={!isNextActive || props.lastPage < 11}
        onClick={onClickNextPage}
        isActive={isNextActive && props.lastPage >= 11}
      >
        &gt;
      </S.Button>
    </S.Wrapper>
  );
}

Pagination.style.ts

import styled from "@emotion/styled";

interface ICurrentProps {
  isCurrent: boolean;
}
interface IActiveProps {
  isActive: boolean;
}

export const Wrapper = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: space-around;
  width: 400px;
  margin-top: 30px;
`;

export const PageNumber = styled.div`
  color: ${(props: ICurrentProps) => (props.isCurrent ? "#8B008B" : "gray")};
  font-family: ${(props: ICurrentProps) =>
    props.isCurrent ? "GmarketSansTTFMedium" : "NanumSquareL"};
  cursor: pointer;
  width: 37px;
  font-size: 15px;
  text-align: center;
  :hover {
    color: #8b008b;
  }
`;
export const PageNumberWrapper = styled.div`
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
  width: 330px;
`;

export const Button = styled.button`
  color: ${(props: IActiveProps) => (props.isActive ? "black" : "white")};
  cursor: ${(props: IActiveProps) => (props.isActive ? "pointer" : "")};
`;
profile
🍓e-juhee.tistory.com 👈🏻 이사중

0개의 댓글