๋ชฉ์ฐจ
๋ฐฑ์ค๋ ๊ณต๋ถํ๋ฉด ๋นผ๋์ ์ ์๋ ๊ฐ๋ , ๋ฐ๋ก ํ์ด์ง!
์ฒ์์ ํ์ด์ง ์ฒ๋ฆฌ๋ฅผ ๋ฐฐ์ ์๋ ๋๋ฆ ์ค-๊ณ ๊ธ์ด๋ผ๋ ๊ต์๋๋ง์ ์ง๋ ๊ฒ๋จน์ ๊ธฐ์ต์ด ์๋ค.
์๋... 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๋ฐฉ์์ผ๋ก! ๊ณ ๊ณ ๊ณ ๐๏ธโ๏ธ
+) ํ์ฌ ํ์ด์ง ์ฟผ๋ฆฌ ์คํธ๋ง๊ณผ ์ฐ๊ฒฐํ ์์ !
์ฐธ๊ณ ๋ธ๋ก๊ทธ