웹 개발을 하다보면, 게시판 혹은 목록을 만들게 되는 일이 빈번하다.
게시판에 표시할 데이터의 수 자체가 적으면 문제가 안되겠지만,
많은 경우에 게시판에 올라가야할 글이 수 천, 수 만이 되는 경우가 허다하다.
하지만 이런 대량의 데이터를 한 번에 전부 가져오는 일은 옳지 못하는 경우게 많다.
실제로, MySql에 저장된 10개의 열을 가진 2000개의 데이터를 단순히 fetching하는 것만으로
도 화면이 조금 끊기는게 보이며, 이는 UX에 좋지 않기 때문이다.
Pagination은 이런 문제에 대한 적합한 해결책이다. 보통 20~100개의 데이터만을 fetching하여 첫 페이지를 보여주고, 사용자가 숫자 버튼을 클릭할 때 마다, 다음 20~100개의 데이터를 fetching해 와 게시판에 표시하는 방식을 택한다.
MySql에 저장되어 있는 자산 데이터들을 가져오기 위해서, useSWR을 활용했습니다.
const { data, error, isValidating, mutate } = useSWR(
`/api/allassets?&page=${state.page}&limit=${state.limit}...`,
fetcher
);
기본적으로 Context API를 통해 정의된 전역 state가 query parameter로 전달되고,
dispatch event를 통해, 변경된 파라미터가 전달되며 새로운 데이터를 가져옵니다.
즉, 페이지네이션의 현재 page와 한 번에 보여질 데이터의 갯수인 limit을 state 변수로 정의해놓고 사용자들이 해당 변수의 상태를 변경할 때 마다, 이에 해당하는 데이터를 가져옵니다.
사용자들이 선택할 limit (데이터가 한 페이지에 보여질 개수) 및 기타 다른 필터에 따라서, 전체 페이지의 개수가 달라지므로, 생성할 총 page의 개수를 자동으로 반환하는 sql query를 데이터를 가져오면서, 함께 포함시킬 수 있으면 pagination 컴포넌트를 만들기 매우 용의하다.
사용자가 선택한 필터에 따라 가져올 총 row의 개수를 반환하는 query를 포함시켰다.
import executeQuery from "datas/mysql";
const handler=async(req,res)=>{
const {page,limit, ...}=req.query
try{
const rowNum=await executeQuery({
query: `SELECT count(*) FROM RMS.ALL_ASSETS
WHERE 1=1
...
//req.query로 넘겨받은 상태 변수들을 where 절에 포함
`;
})
const parsedRow=await JSON.parse(JSON.stringify(rowNum))
const rowCount=parsedRow[0]['count(*)']
const result=await executeQuery({
query: `SELECT al.*,cc.*
FROM RMS.ALL_ASSETS al
LEFT JOIN RMS.CHART_TMP cc
ON cc.ITEM_CD = al.ITEM_CD AND cc.CHART_TP=7
WHERE 1=1
...
//req.query로 넘겨받은 상태 변수들을 where 절에 포함
LIMIT ${limit*(page-1)}, ${limit}
;`
})
res.status(200).json([{assets:[...result]},{rowCount:rowCount}])
} catch(err){
res.status(401).json({ message: "Can't find data" });
}
}
export default handler;
이 코드에서 LIMIT ${limit*(page-1)}, ${limit} 이 pagination의 핵심이라고 말할 수 있다. page와 limit이 변경될 때 마다, 자동으로 그에 맞는 데이터를 가져온다.
Pagination 컴포넌트에 전달한 props는 다음과 같다.
total : 총 데이터의 갯수
page : page 상태 변수
setPage : page를 변경하는 dispatch 핸들러
views : 한 페이지에 보여질 데이터 개수 (limit)
const Pagination = ({ total, page, setPage, views }: Props) => {
const listLimit = views || 10;
const pageLimit = 9;
const startPage =
parseInt((page - 1) / (pageLimit + 1) + "") * (pageLimit + 1) + 1;
const numPages = Math.ceil(total / listLimit);
const endPage =
startPage + pageLimit > numPages ? numPages : startPage + pageLimit;
const pageArray = [];
for (let i = startPage; i <= endPage; i++) {
pageArray.push(i);
}
const handlePrev = () => page !== 1 && setPage(page - 1);
const handleNext = () => page !== numPages && setPage(page + 1);
return (
<main className="w-full flex justify-center">
<div className="flex items-center min-w-[350px] mt-10 mx-auto">
<Image
src={arrow}
alt=""
onClick={handlePrev}
className={`cursor-pointer rotate-180 mr-5 ${
page === 1 && "opacity-30 cursor-default"
}`}
/>
<div className="flex justify-around mx-auto gap-2">
{pageArray.map((i) => (
<div
key={i}
onClick={() => setPage(i)}
className={`flex justify-center items-center w-10 h-10 rounded-20 text-gray-600 text-sm bg-white border cursor-pointer ${
page === i && "bg-[#E6F5FF] border-[#0198FF] text-[#0198FF]"
}`}
>
<p className="h-4">{i}</p>
</div>
))}
</div>
<Image
src={arrow}
alt=""
onClick={handleNext}
className={`cursor-pointer ml-5 ${
page === numPages && "opacity-30 cursor-default"
}`}
/>
</div>
</main>
);
};
export default Pagination;
완성된 paginatino 컴포넌트는 페이지내에서 활용된다.
//... 윗 부분 생략
{assetList.map((asset, i) => (
<Items
exchg={asset['HR_ITEM_NM']}
krName={asset['ITEM_KR_NM']}
riskDescriptionKr={asset['LV_DSCP_KR']}
riskDescriptionEn={asset['LV_DSCP_ENG']}
curr={state.currency}
cat={asset['CAT']}
...
/>
))}
</tbody>
</table>
<Pagination
total={rowCount}
page={state.page}
setPage={(page) => dispatch({ type: 'SET_PAGE', payload: page })}
views={state.limit} />
</div>
Paginatnion 컴포넌트는 대량의 데이터 중 일부를 가져와 화면에 표시하고, 사용자들에게 순차적으로 보여주고자 할 때 적합한 컴포넌트이다. Infinite Loading과 함께 꼭 알아두고 제대로 활용할 수 있도록 공부해두는게 좋다고 생각한다.