페이지 처리를 하는 방법에는 크게 일반적인 방식
과 무한스크롤 방식
, 2가지 방법이 있다.
이번에는 무한스크롤 방식보다 가장 일반적으로 쓰이는 일반적 방식의 페이지 처리, 즉 페이지네이션 방식
부터 알아보자
페이지네이션은 페이지 번호를 클릭해서 이동하는 방식의 페이지 처리 방법이다.
게시판 형태의 페이지에서 가장 일반적으로 사용되는 방식이기도 하다.
사용자들은 페이지네이션을 일상적으로 사용하고 있기 때문에 인식하지 못하지만,
사실 페이지네이션 처리를 위해서는 다양한 것을 고려
해주어야 한다.
그렇기때문에 페이지네이션을 단계적으로 만들면서 고려해야 할 부분들에 대하여 알아보자.
가장 먼저 fetchBoards API를 활용해서 게시글 목록을 불러오는 것이다.
이 때, playground Docs를 참고하여 page 인자도 함께 불러와서 프론트에서 우리가 사용할 수 있게 해준다.
// gql query
const FETCH_BOARDS = gql`
query fetchBoards($page: Int) {
fetchBoards(page: $page) {
_id
writer
title
}
}
`;
// fetchBoards
const { data } = useQuery(FETCH_BOARDS, { variables: { page: 1 } });
return (
<div>
<h1>페이지네이션 연습 !!!</h1>
{data?.fetchBoards?.map((el) => (
<div key={el._id}>
{el.title} {el.writer}
</div>
))}
<span> 1 </span>
<span> 2 </span>
<span> 3 </span>
</div>
)
현재 페이지를 보여줬으나 아직 문제가 많다.
먼저 페이지를 수동으로 1, 2, 3페이지를 만들어주었기 때문에 반복문을 통해 해결해줘야 한다.
또한 이 웹에서는 페이지를 클릭해도 아무런 일이 발생하지 않는다.
이때, 우리는 페이지를 클릭할 때 마다, 그 페이지로 이동하고 싶게 만들고 싶어질 것이다.
위의 문제들을 해결하기 위해 가장 먼저 페이지네이션의 페이지 숫자를 클릭할 때마다
목록에 뿌려진 데이터가 해당 페이지에 해당하는 데이터로 변경되도록 해보자.
graphQL의 useQuery에서 제공하는 refetch라는 함수
를 활용하면
페이지 클릭 시 해당 페이지에 해당하는 데이터를 다시(re) 불러올(fetch) 수 있다.
refetch를 사용하기 위해서는 useQuery에서 data와 함께 refetch라는 함수를 불러와야 합니다.
필요한 부분에 불러온 refetch 함수를 넣어주고,
refetch의 인자
에 변경될 variables (이 경우에는 page를 말한다.)
를 입력해보자
// gql query
const FETCH_BOARDS = gql`
query fetchBoards($page: Int) {
fetchBoards(page: $page) {
_id
writer
title
}
}
`;
// fetchBoards
// data와 함께 refetch를 가져오기
const { data, refetch } = useQuery(FETCH_BOARDS, { variables: { page: 1 } });
const onClickPage = (event) => {
// 위에서 가져온 refetch 사용하기
// 이 때, refetch 를 통해 page 를 불러올 때는 Number 로 바꿔줘야한다.
refetch({ page: Number(event.target.id) });
};
return (
<div>
<h1>페이지네이션 연습 !!!</h1>
{data?.fetchBoards?.map((el) => (
<div key={el._id}>
{el.title} {el.writer}
</div>
))}
<span onClick={onClickPage} id="1"> 1 </span>
<span onClick={onClickPage} id="2"> 2 </span>
<span onClick={onClickPage} id="3"> 3 </span>
</div>
)
1 페이지
를 클릭하면 1 페이지의 게시글 목록
이 화면에 보여지고,
2 페이지
를 클릭하면 2 페이지의 게시글 목록
이,
3 페이지
를 클릭하면 3 페이지의 게시글 목록
이 보여지는 것을 확인하실 수 있습니다.
이제 페이지를 클릭하면 refetch 를 통해 각 게시글 목록을 불러올 수 있게 되었다.
하지만 아직은 부족하다 만일 150페이지가 있다면 span 태그 150개를 만들어야 할까? 당연히 아니다.
이제 map 을 통해 페이지를 10개씩 만들어 뿌려보자.
방금 한 것과 같이 모든 페이지네이션의 숫자를 직접 입력하는 작업은 비효율적이다.
배열과 map
을 사용해 페이지네이션을 뿌려보자
// gql query
const FETCH_BOARDS = gql`
query fetchBoards($page: Int) {
fetchBoards(page: $page) {
_id
writer
title
}
}
`;
// fetchBoards
// data와 함께 refetch 가져오기
const { data, refetch } = useQuery(FETCH_BOARDS, { variables: { page: 1 } });
const onClickPage = (event) => {
// 위에서 가져온 refetch 사용하기
refetch({ page: Number(event.target.id) });
};
return (
<div>
<h1>페이지네이션 연습 !!!</h1>
{data?.fetchBoards?.map((el) => (
<div key={el._id}>
{el.title} {el.writer}
</div>
))}
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((el) => (
<span onClick={onClickPage} id={String(el)} key={el}>
{` ${el} `}
</span>
))}
</div>
)
이 때, 각 페이지의 구분을 위해 refetch는 event의 currentTarget.id 가 필요하다. 그렇디 때문에 모든 span 태그에는 id 값이 필요해진다.
하지만 이렇게 직접 숫자값을 입력하여 만들면 1~10페이지의 페이지네이션밖에 만들지 못한다.
지금까지 만들어온 페이지네이션은 아직도 부족하다.
먼저, 페이지를 이리저리 옮겨다닐 수 있도록 이전페이지, 다음페이지의 기능이 없다는 것.
그리고 배열을 만들어 수동으로 1~10까지의 원소를 넣어 map 을 통해 페이지를 생성하는 것.
일단 위의 문제 중 수동으로 배열을 만드는 것 먼저 해결해보자.
우리는 이전에, map을 사용할 때 python의 enumerate 처럼 index
도 인자로 받아올 수 있다는 사실을 알고있다.
그것을 활용하면 다음과 같은 방식으로 페이지네이션을 만들어줄 수 있다.
const [startPage, setStartPage] = useState(1);
// 먼저 10 size의 Array를 만들어 임의의 값으로 채워준다.
{new Array(10).fill(1).map((_, index) => (
// map 을 통해 밑의 span 태그를 10개 생성된다. 이 때, id값과 key 값을 넣어줘야한다.
<span
onClick={onClickPage}
id={String(index + startPage)}
key={index + startPage}
>
{` ${index + startPage} `}
</span>
))}
페이지의 구분을 위해서 id map 을 통해 뿌려지는 span 태그에는 id 값이 있는 것을 확인해주자.
또한 map 을 통해 복사되기 때문에 각 태그에 고유의 값을 넣어주기 위한 key 값이 필요하다는 것 또한 알아두자.
10개의 원소가 들어갈 수 있는 배열을 생성하고 임의의 값으로 채워 이를 map 으로 span 태그를 생성해 주었다.
이 때, 각 생성되는 span 태그에는 id 와 key 값에는 index + startPage 로 배열이 순회할 때 마다 1~10 까지 자동적으로 만들어지게 하였다.
그리고 이러한 경우에 시작 페이지에 해당하는 startPage의 값을 변경
해주면
현재 페이지 이후의 10페이지, 이전의 10페이지를 불러오는 기능도 구현이 가능해진다.
그렇다면 이제 useState 로 만들어준 startPage를 이용하여 페이지네이션을 더 고도화해보자
import { gql, useQuery } from "@apollo/client"
import { useState } from "react"
const FETCH_BOARDS = gql`
query fetchBoards($page: Int) {
fetchBoards(page: $page) {
_id
writer
title
}
}
`
export default function PageExample() {
const {data, refetch} = useQuery(FETCH_BOARDS, {
variables: {page: 1}
})
// 시작 페이지를 담을 state
const [startPage, setStartPage] = useState(1);
const onClickPage = (event: any) => {
refetch({
page: Number(event.currentTarget.id)
})
}
// 이전 페이지
const onClickPrev = () => {
setStartPage((prev) => prev - 10)
}
// 다음 페이지
const onClickNext = () => {
setStartPage((prev) => prev + 10)
}
return (
<div>
<h1>Pagination Practice</h1>
<div>
<span style={{marginRight: "15px"}} >writer</span>
<span >title</span>
</div>
{/* 게시판 10개씩 불러오기 */}
{data?.fetchBoards?.map((el: any) => (
<div key={el._id}>
<span style={{marginRight: "50px"}} >{el.title}</span>
<span>{el.writer}</span>
</div>
))}
<div style={{backgroundColor: "yellowgreen"}} >
<span onClick={onClickPrev} >이전 페이지</span>
{new Array(10).fill(1).map((_, index) => (
<span
key={index + startPage}
id={String(index + startPage)}
onClick={onClickPage}
style={{margin: "5px"}}
>
{index + startPage}
</span>
))}
<span onClick={onClickNext} >다음 페이지</span>
</div>
</div>
)
}
이러한 방식으로 적당한 페이지네이션을 만들어줄 수 있었다. 단순히 1~10 페이지만 이동할 수 있는것이 아니라
startPage를 통해 만들어준 PrevPage, NextPage 함수를 만들어주었기 때문에 현재 저장되어있는 게시글의 모든 페이지에 접근할 수 있게 되었다.
얼마남지 않았지만 그럼에도 문제는 남아있다.
- 페이지가 무한히 생성되었다. 마이너스 페이지로도 가게되었으며 게시글 수 10개당 1페이지라면 그 이상으로 페이지가 생성되었다.
- refetch 에 문제가 있다. 예를 들어, 1~10페이지에서 다음페이지를 눌렀다면 11~20 페이지로 이동했을 때, 11페이지로 게시글 목록이 함께 이동되게 만들어주어야 한다.
페이지네이션이 마지막 페이지를 훌쩍 지나갔는데도 계속해서 다음 페이지를 문제를 해결해보자
어떻게 하면 마지막 페이지를 코드로 설정해줄 수 있을까?
가장 먼저 DB에 등록된 게시글의 총 개수를 불러와서 마지막 페이지의 값을 구해보자
// fetchBoardsCount API 요청하기
const FETCH_BOARDS_COUNT = gql`
query fetchBoardsCount {
fetchBoardsCount
}
`;
// lastPage 구하기
// 총 게시글이 13632개 라면 페이지는 1364개만 필요하다.
const lastPage = Math.ceil(dataBoardsCount?.fetchBoardsCount / 10);
그리고 이전 페이지, 다음 페이지를 클릭했을 때 실행되는 함수에 조건을 설정해서
1 페이지 미만
그리고 lastPage가 화면에 출력된 이후
로는
이전 페이지, 다음 페이지 버튼이 동작하지 않도록 만들어주자.
const onClickPrevPage = () => {
// startPage가 1이면 하단 스크립트를 실행하지 않고 종료한다.
if (startPage === 1) return;
setStartPage((prev) => prev - 10);
};
const onClickNextPage = () => {
// startPage + 10가 lastPage보다 클 경우 하단 스크립트를 실행하지 않고 종료한다.
if (startPage + 10 > lastPage) return;
setStartPage((prev) => prev + 10);
};
그리고 페이지네이션 map에 조건부 렌더링을 걸어서
lastPage보다 큰 숫자는 출력 되지 않도록
만들어준다.
{new Array(10).fill(1).map((_, index) =>
index + startPage <= lastPage ? (
<span
key={index + startPage}
id={String(index + startPage)}
onClick={onClickPage}
style={{margin: "5px", cursor: "pointer"}}
>
{index + startPage}
</span>
) : (
<span></span>
)
)}
map은 각 span 태그를 10개씩 생성한다.
이 때, index + startPage 역시 각 페이지마다 조건문에 부합하는지 검사하게 되는데
페이지가 lastPage 와 동일하게 되면 그 페이지까지만 생성하고 나머지는 빈 span태그로 생성한다.
우리가 총 게시글의 10을 나누고 올림한 값인 lastPage 를 만들어주고 나서 해야할 일은 2가지다.
- 이전, 다음 페이지 부분은 lastPage 를 이용하여 마지막 페이지 이상으로 넘어가지 못하도록 한다.
- 페이지네이션 map 의 span 태그는 lastPage 보다 큰 숫자는 출력되지 않도록 한다.
이전 페이지, 다음 페이지 버튼을 눌러서 이동할 경우,
11~20 페이지라면 11페이지
로
21~30 페이지라면 21페이지
로 게시글 목록이 함께 이동 되어야 한다.
첫 번째 방법은 refetch를 이용하는 방법이다.
const onClickPrevPage = () => {
// startPage가 1이면 하단 스크립트를 실행하지 않고 종료한다.
if (startPage === 1) return;
setStartPage((prev) => prev - 10);
refetch({ page: startPage - 10 });
};
const onClickNextPage = () => {
// startPage + 10가 lastPage보다 클 경우 하단 스크립트를 실행하지 않고 종료한다.
if (startPage + 10 > lastPage) return;
setStartPage((prev) => prev + 10);
refetch({ page: startPage + 10 });
};
두 번째 방법은 게시글 목록을 불러오기 위하여 useQuery에 넣는 variables에
startPage라는 state를 넣어주는 방법이다.
이렇게 하면 이전 페이지, 다음 페이지를 눌러서 startPage가 변경될 때
마다
바뀐 startPage의 데이터가 새로 뿌려지게 되기에 의도한대로 refetch 된다.
// 기존 useQuery
const { data, refetch } = useQuery(FETCH_BOARDS, {
variables: { page: 1 },
});
// 변경 후 useQuery
const { data, refetch } = useQuery(FETCH_BOARDS, {
variables: { page: startPage },
});
여기까지 끝났다면
아래와 같이 게시판에서 쓰이는 페이지네이션이 완성된 것을 확인할 수 있다.
추가학습 해보기
게시판을 이용하는 사용자가 현재 페이지가 몇 페이지인지 알 수 있어야한다.
페이지네이션에서 현재 페이지를 다른 페이지와 다르게 표시해보기
이전에 배웠던 emotion에 props를 넘겨주는 방법을 활용할 것
(그 밖에도 다양한 방법으로 구현이 가능)