Pagination + 검색 기능 만들기
Pagination 구현을 위해 고민한 기록
+) 2024. 01. 17. 업데이트
Pagination 라이브러리를 배포하였습니다. 필요하신 분들은 사용해보세요! 😀
https://inthedev.tistory.com/104
여러 아이템을 페이지로 구분하는 페이지네이션 기능을 구현하기 위해 많은 고민을 하였다. 아이템을 12개씩 한 페이지로 하고 싶어서 아이템에 index를 부여하여 아이템 개수를 계산했다. 0~11번 아이템은 1페이지, 12~23번 아이템은 2페이지, ... 이렇게 코드를 작성했다. 여기까지는 별 문제 없었으나, 문제는 검색 기능과 같이 쓸 때 있었다. 특정 키워드로 검색을 하고 나면 아이템의 개수를 index로 구분할 수 없었다.
아이템을 특정 키워드로 검색하고 나면 index에 규칙이 없다. 예를 들어 제목에 2023이 포함된 아이템을 검색하고 나면, 그에 해당하는 아이템들을 그냥 나열하는 것까지는 가능해도 몇 번부터 몇 번까지 1페이지에 넣어달라는 코드를 작성할 수가 없는 것이다. index로 개수를 파악할 수 없으니까🤔 그래서 페이지네이션을 구현하기 위한 방법을 두 가지로 나누어 생각해보았다.
# 첫 번째 방법
1. 일단 아이템들을 모두 불러온 다음, 전부 CSS로 display: none; 으로 설정해서 안 보이게 하기
2. 원하는 아이템들만 CSS로 display: none; 없애서 보여주기
아이템을 모두 불러온 뒤 CSS로 화면에서 안 보이게 가려준다. 그리고 1페이지를 클릭했을 때는 1~12번째의 아이템의 display: none;을 없애서 화면에 보여주고, 2페이지에서는 13~24번째의 display: none;을 없애서 화면에 보여주는 방식을 사용한다. 아이템의 index가 차례대로 정렬되었을 때만 쓸 수 있다.
# 두 번째 방법
1. 빈 배열 하나 만들기
2. 원하는 아이템들을 배열에 넣기
3. 그 배열을 map으로 html에 표현하기
특정 조건에 맞는 아이템만 배열에 모아서 화면에 보여주는 방식이다. 아이템의 index가 차례대로 정렬되어 있지 않아도 사용할 수 있다.
첫 번째 방법은 일단 아이템을 모두 화면에 불러온 뒤 CSS로 원하는 아이템만 보이게 하는 방법이고, 두 번째 방법은 원하는 아이템만 화면에 불러오는 방법이다. 둘 다 해본 결과 각각의 장단점이 있었다. 일단 첫 번째 방법은 아이템이 너무 많을 경우 웹페이지에 무리가 갈 수 있다는 단점, 두 번째 방법은 계속해서 새로운 배열을 생성해내는 방법이다 보니 로딩이 좀 걸린다는 단점이 있다.
내가 만든 웹페이지는 아이템의 개수가 그렇게 많지 않다.(100개 정도) 그렇기 때문에 기본적으로는 첫 번째 방법을 사용하지만 검색 후 원하는 아이템만 화면에 보여줄 때는 두 번째 방법을 사용한다. 한 마디로, 아무 검색을 하지 않고 그냥 나열된 아이템을 보는 경우는 첫 번째 방법, 검색을 해서 특정 키워드의 아이템을 보여주는 경우는 두 번째 방법을 이용한다.
# 첫 번째 방법 코드
첫 번째 방법의 내용은 다음과 같다.
우선 아이템을 다 불러온다.
페이지 번호가 1일 때, index 값이 0 ~ 11인 아이템을 보여준다.
페이지 번호가 2일 때, index 값이 12 ~ 23인 아이템을 보여준다.
즉, 페이지 번호가 n일 때, index 값이 (n-1)*12 ~ n*12-1인 아이템을 보여준다.
코드가 간단하다. 우선 아이템을 map으로 html에 표현해준다음 CSS로 display: none;을 해준다. 그리고 페이지 번호를 클릭할 때마다 해당하는 아이템의 display: none;을 제거해주는 코드를 작성해주면 된다. 나는 Tailwind CSS를 사용하였으므로, display:none;은 className에 "hidden"을 작성하여 구현하였다. (display:none; = hidden)
1. html 부분 작성
const dataRef = useRef();
<div ref={dataRef}>
{data.map((item, index) => (
<div className={index < 12 ? "w-[100px] h-[100px]" : "hidden"}>item</div>
)}
</div>
html로 아이템을 모두 불러온다. 나는 한 페이지에 12개의 아이템을 보여줄 것이다. 그러므로 가장 첫 화면, 즉 1페이지의 아이템에는 hidden을 넣어줄 필요가 없다. 그래서 아이템의 index가 12보다 작으면 hidden을 넣지 말라고 해두었다. 그리고 아이템의 상위 컴포넌트에 ref를 달아준다. ref는 이후에 페이지 버튼 클릭 함수에 사용할 예정!
2. 전체 페이지 계산 및 페이지 리스트 만들기
페이지 리스트란 화면 하단의 현재 페이지를 나타내주는 부분을 말한다. 페이지 리스트를 구현하기 위해서 아이템의 개수를 세고 전체 페이지 수를 계산한다.
const [totalPage, setTotalPage] = useState();
const [pageList, setPageList] = useState([]);
useEffect(() => {
if (data) {
setTotalPage(Math.ceil(data.length / 12));
}
}, [data]);
useEffect(() => {
let pageItems = [];
for (let i = 1; i <= totalPage; i++) {
pageItems.push(i);
}
setPageList(pageItems);
}, [totalPage]);
아이템 개수를 12로 나누어 총 페이지 수를 계산하고 페이지 리스트를 배열로 담아준다.
{pageList.map((item, index) => (
<div onClick={(event) => setPage(parseInt(event.target.innerText))}>{item}</div>
))}
html에 페이지 리스트를 작성해준다. 그리고 페이지 번호를 클릭하면 그 번호를 page 변수에 저장한다. 이때 page를 숫자 타입으로 바꾸어 저장해준다. string 타입이면 나중에 오류가 날 수도😵
3. 페이지 번호 클릭 함수 만들기
페이지 번호가 n일 때, index 값이 (n-1)*12 ~ n*12-1인 아이템을 보여주는 함수를 만든다.
useEffect(() => {
for (let j = 0; j < data.length; j++) {
dataRef.current.children[j].classList.add("hidden");
if (page !== totalPage) {
for (let i = (page - 1) * 12; i < page * 12; i++) {
dataRef.current.children[i].classList.remove("hidden");
}
} else if (page === totalPage) {
for (let i = (page - 1) * 12; i < Prev.length; i++) {
dataRef.current.children[i].classList.remove("hidden");
}
}
}
}, [page]);
page 값이 변할 때마다 보여주는 아이템을 변경한다. dataRef의 children이 곧 아이템이므로, dataRef.current.children[i]의 className에 hiddne을 제거해주면 해당 아이템이 화면에 보이게 된다. 단, 마지막 페이지에서는 아이템 개수가 12개 보다 작을 수 있으므로 따로 경우를 빼서 계산해준다. 아이템이 12개 보다 적은 상태에서 아이템 12개에 대한 동작을 해달라는 코드를 작성하면 에러가 뜬다.
이렇게 작성해주면 첫 번째 경우에 대한 코드 작성은 끝난다. 그냥 일반적으로 '페이지로 나뉜 아이템'을 보여주고 싶을 때 사용할 수 있는 코드이다. 이제 '특정 키워드로 검색한 뒤, 규칙적이지 않은 index를 가진 아이템들'을 페이지로 나누어 보여주는 코드를 알아보자.
# 두 번째 방법 코드
특정 제목을 가진 아이템을 검색하고 난 뒤의 그 아이템들만 모아보자. index에는 규칙이 없다. 따라서 index를 이용하는 첫 번째 방법을 사용할 수 없다. 그래서 아예 빈 배열을 새로 하나 만들고, 그 배열에 아이템들을 넣은 다음, 그 배열의 길이를 이용하여 검색 후 아이템들의 페이지 리스트를 만들 것이다.
const [searchedData, setSearchedData] = useState([]);
const handleSubmit = (event) => {
try {
// step 1
let cnt = 0;
let dataItem = [];
let dataItems = [];
for (let i = 0; i < data.length; i++) {
if (data[i].NAME.includes(searchText)) {
dataItem.push(data[i]);
cnt = cnt + 1;
}
}
// step 2
if (cnt === 0) {
return setSearchedData(undefined);
}else {
setTotalPage(Math.ceil(cnt / 12));
}
// step 3
let i = 0;
let dataArray = [];
while (i < totalPage) {
if (i === totalPage - 1) {
for (let j = i * 12; j < cnt; j++) {
dataItems.push(dataItem[j]);
}
} else {
for (let j = i * 12; j < (i + 1) * 12; j++) {
dataItems.push(dataItem[j]);
}
}
dataArray.push(dataItems);
i++;
}
//step 4
setSearchedData(dataArray);
} catch (error) {
console.log(error);
}
};
Step1
cnt : 아이템의 개수를 세는 변수이다. 검색 결과로 얻은 아이템으로 새로 페이지 리스트를 만들 때 쓴다.
dataItem : 검색 결과에 맞는 아이템만 모으는 배열이다.
dataItems : 검색 결과에 맞는 아이템을 페이지로 나눌 때 사용하는 배열이다. dataItem의 0~11번 아이템은 1페이지, dataItem의 12~23번 아이템은 2페이지 이렇게 나눌 때 사용하는 배열!
for문 : 특정 조건을 작성하고, 그 조건에 해당하는 아이템만 dataItem 배열에 넣는다.
Step 2
만약 cnt가 0이면? 검색 결과에 해당하는 아이템이 없다는 의미가 된다. 검색 결과 없음을 보여주기 위해 searchData에 undefined을 할당해준다. 0이 아니라면? 검색 결과에 해당하는 아이템을 모아 새로운 totalPage를 계산한다.
Step 3
i : while문에서 사용할 변수이다.
dataArray : 최종적으로 화면에 보여줄 아이템들을 담는 변수이다. 1페이지를 클릭했다면 페이지가 1일 때의 dataItems 배열을 보여줘야 하고, 2페이지를 클릭했다면 페이지가 2일 때의 dataItems 배열을 보여줘야 한다. 이 dataItems를 담을 배열이다.
while문 : dataArray를 생성하기 위한 코드이다. 검색 결과에 해당하는 아이템이 dataItem에 담겼었다. 이 dataItem의 0~11번째 원소를 dataItems[0]에 담는다. dataItem의 12~23번째 원소를 dataItems[1]에 담는다. 즉, dataItem의 (n-1)*12 ~ n*12-1번째 아이템을 dataItems[n]에 담는다. 그리고 이렇게 생성된 dataItems를 dataArray에 담기고, dataArray는 2차원 배열이 된다.
item 하나하나 : dataItem
item 담긴 배열 : dataItems
전체 배열 : dataArray
Step 4
완성된 dataArray를 실제 html에서 사용할 searchData 변수에 넣어준다.
이렇게 하면 검색 결과 아이템들을 페이지로 나누어 계산할 수 있다. 검색 결과 20개의 아이템이 나왔다면 새로운 페이지 리스트를 생성하고 1페이지에는 12개, 2페이지에는 8개의 아이템이 담긴 페이지가 완성된다.
첫 번째 방법이 두 번째 방법에 비해 로딩 속도가 확실히 빠르다. 두 번째 방법은 클릭할 때마다 새로운 배열을 생성하기 때문에 로딩 속도가 눈에 보일 정도로 좀 느리다. 그러나 첫 번째 방법만을 이용하면 검색 기능과 같이 사용할 수 없으므로, 두 방법을 적절히 섞어서 프로젝트에 적용하였다.
'개발 이야기 > 직접 해보기' 카테고리의 다른 글
[JavaScript] CommonJS와 ESM을 모두 지원하는 React 라이브러리 개발 (Feat. 모듈이란 무엇인가) (0) | 2023.12.09 |
---|---|
[CSS] 마우스를 올리면 움직이는 3D 책 만들기 (0) | 2023.11.20 |
[React] 반응형 디자인 직접 코드로 구현해보기 (0) | 2023.08.09 |
[React Query] useQuery로 검색 기능을 만들던 중 마주친 문제 (0) | 2023.05.15 |
[React] 페이지 이동 후 스크롤 유지하기 (0) | 2022.12.21 |