목차
백앤드 공부하면 빼놓을 수 없는 개념, 바로 페이징!
처음에 페이징 처리를 배웠을때 나름 중-고급이라는 교수님말에 지레 겁먹은 기억이 있다.
아니... DB 불러오면 끝아닌가? 굳이 페이징 처리를 주어야 하나? ---쪼쪼쪼렙 시절
그래서 Database 같은 자돌 라이브러리만 쓰고 도전을 안했다.
하지만 언젠가 해야할거, 이번에 빠르게 정리해두자! 하는 마음으로 쓰는 글이다.
이번 프로젝트는 React/Node.js 기준이다!
Intro🎈 페이징의 필요성
사실 다 아는 내용이지만, DB에서 모든 정보를 몽!땅! 가져온다는건 무리이다.
쿼리문 자체가 느린 작업인데, 10,000건, 1,000,000건 늘어날지도 모르는 칼럼(데이터)들을 한 번에 가져와
프론트에서 랜더링한다? 무조건 로딩이 길어진다.
DB에 약 10,000건의 데이터를 불러오도록 해보았다.
페이징 처리하지 않은 경우는 약 5초, 처리한 경우는 1초 채 되지않았다.
Offset, Limit 기준 페이징
offset
: 시작하는 숫자 limit
: 끝나는 숫자
이 두 숫자를 기준으로 뭉텅 뭉텅 읽어오는 것이다.
예를 들어, 1번째 - 5번째 칼럼을 읽어와라는 (offset 0, limit 5)로 표현한다.
*0부터 시작하는걸 잊지말자!
SELECT * FROM tb_artwork
LIMIT 0, 5 ;
SELECT * FROM tb_artwork
LIMIT 1, 5 ;
Paging 결론 부터
👇Paging Server Side 코드 전문
Paging처리에 필요한 offset과 limit을 계산하는 모듈.
exports.pagingServerSide = (curpage, pageSize) => {
const DEFAULT_START_PAGE = 1;
const DEFAULT_PAGE_SIZE = 5;
if (!curpage || curpage <= 0)
curpage = DEFAULT_START_PAGE
if (!pageSize || pageSize <= 0)
pageSize = DEFAULT_PAGE_SIZE
let result = {
offset: (curpage - 1) * Number(pageSize),
limit: Number(pageSize)
}
return result
}
// dao 작업
artworkDao = {
getArtworks: async(query) => {
try {
console.log('Artworks 작품 리스트 요청', query)
let paging = pagingServerSide(query.page, query.pageSize)
const artworks = await Artwork.findAll({
where: setArtworkCondition(query),
order: setArtworkOrder(query.order, query.orderBy),
offset: paging.offset,
limit : paging.limit
});
apiRetuner.setHeader('true', null, 'Success to get Artworks info', 200)
apiRetuner.setArtworkObj(artworks);
} catch (error) {
apiRetuner.setHeader('false', 'Server Error', error.toString(), 500)
}
return apiRetuner.getReturnData();
},
}
// artwork paging 정보 리턴
getArtworksPagingInfo: async(query) => {
try {
const info = await Artwork.findAll({
attributes: [
[Sequelize.fn('COUNT', Sequelize.col('Artwork_ID')), 'count'],
],
where: setArtworkCondition(query),
})
apiRetuner.setHeader('true', null, 'Success to get Artworks info', 200)
apiRetuner.setData({ allSize : info[0].dataValues.count });
} catch (error) {
console.log(error)
apiRetuner.setHeader('false', 'Server Error', error.toString(), 500)
}
return apiRetuner.getReturnData();
},
👇Paging Front Side 코드 전문
Paging 모듈을 만들어서, 한번에 page 수들을 계산해 리턴한다.
export default class Paging {
constructor(allSize, pageSize, navSize) {
this.allSize = Number(allSize)
this.pageSize = Number(pageSize)
this.navSize = Number(navSize)
this.lastPage = Math.ceil(allSize / pageSize)
this.startPage = 1;
this.endPage = 1;
this.nextPage = 1;
this.backPage = 1;
this.jumpPage = 1;
this.jumpBackPage = 1;
}
judgePage(_page) {
if (!_page || _page <= 0)
_page = 1
else if (_page > this.lastPage)
_page = this.lastPage
return _page
}
getPaging(curPage) {
curPage =this.judgePage(Number(curPage));
let gap = curPage%this.navSize ===0 ? this.navSize - 1 : curPage%this.navSize - 1
let startPage = this.judgePage(curPage - gap);
let endPage = startPage + this.navSize - 1;
let result = {
curPage: curPage,
startPage: startPage,
endPage: this.judgePage(endPage),
nextPage: this.judgePage(++curPage),
backPage: this.judgePage(--curPage),
jumpPage: this.judgePage(++endPage),
jumpBackPage: this.judgePage(--startPage)
}
console.log('getPaging', result)
return result;
}
}
// 활용 예시
const paging = new Paging(allSize, CONFIG.PAGING.pageSize, CONFIG.PAGING.navSize)
let result = paging.getPaging(curPage, startPage, endPage)
코드 풀이
Paging Server Side
// 현재 페이지, 한 페이지당 보여줄 페이지 수
exports.pagingServerSide = (curpage, pageSize) => {
const DEFAULT_START_PAGE = 1;
const DEFAULT_PAGE_SIZE = 5;
if (!curpage || curpage <= 0)
curpage = DEFAULT_START_PAGE
if (!pageSize || pageSize <= 0)
pageSize = DEFAULT_PAGE_SIZE
let result = {
offset: (curpage - 1) * Number(pageSize),
limit: Number(pageSize)
}
return result
}
서버 사이드에선 offset과 limit를 구해야한다.
만약 한 페이지당 보여줄 게시물의 수가 10개라고 하자. 현재 페이지가 1 이면, 0 ~ 10 / 현재 페이지가 2라면 10 ~ 20.
때문에 위 식이 나오게 된다.
// artwork paging 정보 리턴
getArtworksPagingInfo: async(query) => {
try {
const info = await Artwork.findAll({
attributes: [
[Sequelize.fn('COUNT', Sequelize.col('Artwork_ID')), 'count'],
],
where: setArtworkCondition(query),
})
apiRetuner.setHeader('true', null, 'Success to get Artworks info', 200)
apiRetuner.setData({ allSize : info[0].dataValues.count });
} catch (error) {
console.log(error)
apiRetuner.setHeader('false', 'Server Error', error.toString(), 500)
}
return apiRetuner.getReturnData();
},
전체 페이지 수를 알아야, 프론트 사이드에서 lastPage와 navSize 기준으로 묶음을 나눌 수 있기 때문에, count만 리턴하는 api를 따로 빼주었다. findAll 하는 것 보다 시간을 단축할 수있음!
Paging Front Side
프론트 사이드에선, 사용자 인터렉션에 따라 달라져야할 현재 페이지 번호(curPage), 페이지네이션의 시작 번호(startPage)와 끝 번호(endPage), 다음 페이지 버튼이 가질 페이지 숫자(nextPage, backPage, jumpPage, jumpBackPage)를 리턴해야한다. 컴포넌트 코드에 계산하는 함수를 쓰면 가독성이 안 좋기때문에, 모듈로 빼주었다.
// allSize : 모든 데이터 수
// pageSize : 한 페이지에서 보여줄 데이터 수
// navSize : 페이지네이션이 보여줄 최대 페이지 수
constructor(allSize, pageSize, navSize) {
this.allSize = Number(allSize)
this.pageSize = Number(pageSize)
this.navSize = Number(navSize)
this.lastPage = Math.ceil(allSize / pageSize)
this.startPage = 1;
this.endPage = 1;
this.nextPage = 1;
this.backPage = 1;
this.jumpPage = 1;
this.jumpBackPage = 1;
}
Paging을 생성할때 미리 정의해둔 페이징의 기준 값을 가져온다. 즉 api/user/paging api를 통해 가져온 allSize (모든 데이터 수)를 가져와 변수로 넣어준다. 위 세가지 매개변수가 있어야 페이징을 계산할 수 있다.
lastPage는 모든 데이터를 페이지네이션 사이즈로 나눈 수로, 가장 끝에 보여줄 숫자이다.
judgePage(_page) {
if (!_page || _page <= 0)
_page = 1
else if (_page > this.lastPage)
_page = this.lastPage
return _page
}
만약 page가 없거나, 음수일 경우 1로 세팅해주고, 마지막 페이지보다 크면, 다시 마지막 페이지로 설정해준다.
이 판단 함수는 계속 쓰이므로 함수로 빼주었다.
getPaging(curPage) {
curPage =this.judgePage(Number(curPage));
let gap = curPage%this.navSize === 0 ? this.navSize - 1 : curPage%this.navSize - 1
let startPage = this.judgePage(curPage - gap);
let endPage = startPage + this.navSize - 1;
let result = {
curPage: curPage,
startPage: startPage,
endPage: this.judgePage(endPage),
nextPage: this.judgePage(++curPage),
backPage: this.judgePage(--curPage),
jumpPage: this.judgePage(++endPage),
jumpBackPage: this.judgePage(--startPage)
}
console.log('getPaging', result)
return result;
}
실질적인 페이징 정보를 가져올 getPaging 함수!
startPage를 구하는게 살짝 까다로웠다. 페이지네이션의 시작 숫자는 다음과 같다.
만약 navSize가 5이고, 현재 페이지(curPage)가 5 라면 startPage = 5 - 4
현재 체이지가 3 이라면 startPage = 3 - 2
navSize를 넘어가도 마찬가지로 -4, -3, -2, -1 을 계산해야 한다.
즉! startPage는 현재 페이지를 navSize로 나눈 나머지 값에 1을 빼야 함.
그런데 문제. 만약 나머지가 0일 경우에도 시작페이는 -4이 돼야 한다.
이를 위해 삼항 연산으로 나머지가 0일 경우와 아닐 경우를 나눴다.
페이지니네이션에서 보이는 << 와 >> 기능을 넣어보자.
이전에 구해둔 startPage와 endPage를 통해 추가하면 된다.
jumpBackPage=startPage-1
jumpPage=endPage-1
한 후, judgePage()
함수로 검증하면 끝!
최종 활용 예시
import Paging from '../utils/paging';
const TablePagination = ({allSize, curPage, onClickNavItem}) => {
const [paging, setPaging] = useState(0);
useEffect( async () => {
const pagingSystem = new Paging(allSize, CONFIG.PAGING.pageSize, CONFIG.PAGING.navSize)
const query = await queryString.parse(window.location.search)
if(!allSize) return
let result = pagingSystem.getPaging(curPage)
if(!result) return
setPaging(result)
}, [allSize, curPage])
let renderNavItem = () => {
const result = []
for (let i = paging.startPage; i <= paging.endPage; i++) {
result.push(
<li
key={i}
className={i === Number(curPage)
? "page-item active"
: "page-item"}>
<span className="page-link" onClick={onClickNavItem} data-page={i}>{i}</span>
</li>
)
}
return result
}
return (
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item">
<span
class="page-link"
href="#"
aria-label="Previous"
data-page={paging.jumpBackPage}
onClick={onClickNavItem}>
<span aria-hidden="true" data-page={paging.jumpBackPage}>«</span>
</span>
</li>
{paging && renderNavItem()}
<li class="page-i.startPage tem">
<span
class="page-link"
href="#"
aria-label="Next"
data-page={paging.jumpPage}
onClick={onClickNavItem}>
<span aria-hidden="true" data-page={paging.jumpPage}>»</span>
</span>
</li>
</ul>
</nav>
);
}
Offset 방식의 한계
- 느린 속도
- 실시간 update를 반영하지 못한다!
offset은 db에서 모든 컬럼을 읽어와, n~m 순서를 부여한 후 offset부터 limit 수로 자르는 작업이다.
계속해서 모든 컬럼 내용을 읽어와야하기 때문에, 인덱스를 활용하는 cursor보다 느리다.
또, offset은 한번 데이터를 부른 후, 다른 사용자가 insert를 하거나, delete를 하면 봤던 데이터를 또 보는 경우가 생긴다. 때문에, 중복된 데이터를 보지않고 사용자가 연속적인 경험을 할 수 있으려면 cursor기반 Paging이 필요하다.
아무튼, 이 두 단점을 보완한 방식이 cursor 방식이다.
전체 페이지 수를 보고, n번부터 m번을 찾는게 offset이라면,
cursor 방식은 책에 책갈피를 하고 한번에 여는 방식이라고 이해하면 된다.
2탄은 cursor방식으로! 고고고🏌️♀️
+) 현재 페이지 쿼리 스트링과 연결할 예정!
참고 블로그
'Dev > 코딩공부 이모저모' 카테고리의 다른 글
CS ) ssh, sh, bash 란 뭘까? (0) | 2021.09.25 |
---|---|
CS) 서버 OS 알아보기, CentOS, Linux, Ubuntu, Window (0) | 2021.09.05 |
CDN ) CDN 결과 비교하기, 근데 거기에 Request header 분석을 곁들인... (0) | 2021.09.04 |
Node.js ) Storage 에서 사용하지 않는 더미 파일들 정리하기 -- ft.서버 스케줄링 (0) | 2021.08.22 |
React Router ) Link 기능을 함수로 빼기, history와 location (0) | 2021.07.11 |