[Nodejs/Mysql] ๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง•ํ•˜๊ธฐ(1) : LIMIT์„ ์‚ฌ์šฉํ•œ Offset-based Pagination ๊ตฌํ˜„ํ•˜๊ธฐ

2021. 1. 8. 15:00ใ†๐ŸŒฑ Develop/Server

โ“ Pagination (ํŽ˜์ด์ง€๋„ค์ด์…˜)

๋ฐฑ์—”๋“œ์—์„œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๊ฐ’์„ ์ „๋‹ฌํ•  ๋•Œ, ์ผ์ • ๊ธฐ์ค€์œผ๋กœ ๋ถ„ํ• ํ•˜์—ฌ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์›น์—์„œ ๊ฒŒ์‹œ๊ธ€์„ 1ํŽ˜์ด์ง€, 2ํŽ˜์ด์ง€๋กœ ๋„˜๊ธฐ๋Š” ๊ฒƒ๊ณผ '๋”๋ณด๊ธฐ' ๋ฒ„ํŠผ์œผ๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค์„ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ ๋ชจ๋‘ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ํ†ตํ•ด ๊ตฌํ˜„๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋„ค์ด์…˜์˜ ๋ฐฉ์‹์—๋Š” ํฌ๊ฒŒ 2๊ฐ€์ง€๊ฐ€ ์žˆ๋Š”๋ฐ, ํ•˜๋‚˜๋Š” offset ๋ฐฉ์‹์ด๊ณ  ํ•˜๋‚˜๋Š” cursor ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

1. ์˜คํ”„์…‹ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜(offset pagination)

  • limit, offset ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌ๋ถ„ํ•จ.
  • ์ฃผ๋กœ ์›น ํŽ˜์ด์ง€์—์„œ << 1 | 2 | 3 >> ์ด๋Ÿฐ ๋ฐฉ์‹์˜ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ์“ธ ๋•Œ ์‚ฌ์šฉ

2. ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง€๋„ค์ด์…˜(cursor pagination)

  • ๊ฐ€์žฅ ์ตœ๊ทผ idx๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ตฌ๋ถ„ํ•จ.
  • ์ฃผ๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค์ด๋‚˜ ๋”๋ณด๊ธฐ๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ ์‚ฌ์šฉ(SNS)

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ํŽ˜์ด์ง• ๋ฐฉ์‹์ธ offset๋ถ€ํ„ฐ ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

1๏ธโƒฃ Offset Pagination์œผ๋กœ ํŽ˜์ด์ง•ํ•˜๊ธฐ

offset pagination์€ ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐ’์—์„œ "offset๋ถ€ํ„ฐ limit๊นŒ์ง€" ๋งŒ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์€ select๋ฌธ์„ ์ผ์„ ๋•Œ aritst ํ…Œ์ด๋ธ”์—์„œ๋Š” artist_idx๊ฐ€ 1๋ถ€ํ„ฐ 5๊นŒ์ง€์˜ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์ค๋‹ˆ๋‹ค.

select * from articles LIMIT 0,5 

์ด์ œ ์ผ์ •ํ•˜๊ฒŒ ํŽ˜์ด์ง€๋กœ ๋ฐ›์•„๋ณด๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋จผ์ € , ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ url๋กœ ์ผ์ •ํ•˜๊ฒŒ ์š”์ฒญ์ด ๋“ค์–ด์˜จ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

/artist/list?page=1&pageSize=5

page๋Š” ์–ด๋””์„œ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜ฌ์ง€ ์‹œ์ž‘ํ•  offset์„ ๊ณ„์‚ฐํ•  ๊ฐ’์ด๊ณ , pageSize๋Š” offset์—์„œ๋ถ€ํ„ฐ ๋ช‡ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ์ง€๋ฅผ ์ œํ•œํ•˜๋Š” ์ˆ˜์ž…๋‹ˆ๋‹ค. ์ฆ‰, select ๋ฌธ์—์„œ "LIMIT start,end"์—์„œ ๊ฐ๊ฐ start์™€ end์— ๋“ค์–ด๊ฐˆ ์ˆซ์ž๋ผ๊ณ  ์ƒ๊ฐํ•˜์‹œ๋ฉด ์‰ฝ์Šต๋‹ˆ๋‹ค.

 

 

๐Ÿ“‚ Controller/artist.js

๋จผ์ € Controller์—์„œ๋Š” req.query๋กœ page์™€ pageSize๋ฅผ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค. ๊ฐ’ ์ฒ˜๋ฆฌ๋Š” service๋กœ ๋„˜๊ฒจ์„œ ์ฒ˜๋ฆฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

exports.artist = async (req, res) => {
  const pageInfo = req.query;
  const page = parseInt(pageInfo.page);
  const pageSize = parseInt(pageInfo.pageSize);
  const artistName = pageInfo.name;

  try {
    if (!pageInfo || !currentId || !pageSize) {
      res.status(sc.BAD_REQUEST).send(au.successFalse(rm.NULL_VALUE));
    }
    **const artist_list = await userService.cursorArtist(page, pageSize, artistName);**
    artist_list == false ? res.status(sc.BAD_REQUEST).send(au.successFalse(rm.DB_NOT_MATCHED_ERROR)) : res.status(sc.OK).send(au.successTrue(rm.DB_SUCCESS, artist_list));
  } catch (err) {
    res.status(sc.INTERNAL_SERVER_ERROR).send(au.successFalse(rm.INTERNAL_SERVER_ERROR));
    throw err;
  }
};

 

๐Ÿ“‚ Service/artist.js

๋จผ์ € page๋Š” "์–ด๋””์„œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ• ์ง€"๋ฅผ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ๋Š” ์ˆ˜๋ผ๊ณ  ์ƒ๊ฐํ•ด๋ด…์‹œ๋‹ค. ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๊ฐ–๊ณ  ์˜ค๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ด์ „ ํŽ˜์ด์ง€๊ฐ€ ๋๋‚œ ์œ„์น˜๋ฅผ ์•Œ์•„์•ผํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๊ฑธ ๊ณ„์‚ฐํ•˜๋Š” ๊ฒƒ์ด ํŽ˜์ด์ง•์˜ ๊ด€๊ฑด์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๊ฐ„๊ณผํ•˜๊ธฐ ์‰ฌ์šด ๋ถ€๋ถ„์€ DB์—์„œ LIMIT์œผ๋กœ ๊ฐ’์„ ๋ถˆ๋Ÿฌ์˜ฌ ๋•Œ๋Š” 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, page =1์ด ๋“ค์–ด์˜ค๋ฉด ๋ฐฑ์—”๋“œ์—์„œ๋Š” 0๋ถ€ํ„ฐ ์‹œ์ž‘์œผ๋กœ ์ƒ๊ฐํ•ด์•ผํ•œ๋‹ค๋Š” ๊ฑฐ์ฃ .

exports.getArtist = async (page, pageSize) => {
  try {
    let start = 0;

    if (page <= 0) {
      page = 1;
    } else {
      start = (page - 1) * pageSize;
    }

    const cnt = await user.Cnt(data);
    if (page > Math.round(cnt[0].total / pageSize)) {
      return null;
    }

    const artist_list = await user.pageAll(start, pageSize);
    return artist_list;
  } catch (err) {
    console.log('์•„ํ‹ฐ์ŠคํŠธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ.', err);
    throw err;
  }
};

page =1 , pageSize =5 ์ผ ๋•Œ LIMIT 0,5

page =2, pageSize =5 ์ผ ๋•Œ LIMIT 5,5

page =3, pageSIze = 5 ์ผ ๋•Œ LIMIT 10,5 ....์ด๋Ÿฐ ์‹์œผ๋กœ ๊ฐ’์ด ๋“ค์–ด๊ฐ€์•ผํ•ฉ๋‹ˆ๋‹ค.

 let start = 0; 

    if (page <= 0) { // 0 ์ดํ•˜์˜ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๋ฉด
      page = 1;  // 1ํŽ˜์ด์ง€๋กœ ๊ฐ€๋„๋ก ํ•œ๋‹ค.
    } else {
      start = (page - 1) * pageSize;
    }

๊ฒฐ๊ตญ ๋‹ค์Œ ํŽ˜์ด์ง€์˜ ์‹œ์ž‘์ ์€ (page-1) * pageSize๋ผ๋Š” ๊ฒฐ๋ก ์ด ๋‚˜์˜ต๋‹ˆ๋‹ค. pageSize๋Š” ์–ผ๋งˆ๋งŒํผ ๊ฐ€์ ธ์˜ฌ์ง€์— ๋Œ€ํ•œ ๋ฌธ์ œ์ด๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ๋„ฃ์–ด์ฃผ์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง•์„ ๊ณ„์‚ฐํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์–‘ํ•œ๋ฐ, ์ด๋ฒˆ ๊ฒฝ์šฐ๋Š” pageSize๋„ ๊ฐ™์ด ๋ณ€์ˆ˜๋กœ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์— pageSize๋Š” ๋”ฐ๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ sql๋ฌธ์— ๋„ฃ์–ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

 

 

โ—๏ธ์ด์ œ ๊ฑฐ์˜ ๋‹ค์™”์Šต๋‹ˆ๋‹ค.

ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๋‹ค๋ณด๋ฉด, ํด๋ผ์ด์–ธํŠธ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋ช‡ ๊ฐœ๋‚˜ ์žˆ๋Š”์ง€ ๋ชจ๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ์ดํ„ฐ๋ณด๋‹ค ๋งŽ์€ ์ˆซ์ž์˜ ํŽ˜์ด์ง€๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. artist ํ…Œ์ด๋ธ”๋กœ ์˜ˆ๋ฅผ ๋“ค์ž๋ฉด, ๋ฐ์ดํ„ฐ๋Š” 20๊ฐœ๊ณ  1ํŽ˜์ด์ง€ ๋‹น 5๊ฐœ์”ฉ ์š”์ฒญํ•œ๋‹ค๋ฉด ์ด 4ํŽ˜์ด์ง€๊ฐ€ ์ „๋ถ€์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋ฅผ ๋ชจ๋ฅด๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ 5ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•œ๋‹ค๋ฉด ์—๋Ÿฌ๊ฐ€ ๋‚  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

const cnt = await user.Cnt(data);  // selectํ•œ ์ „์ฒด ๊ฒฐ๊ณผ ์ˆ˜

if (page > Math.round(cnt[0].total / pageSize)) { 
    return null; 
}

ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ•œ page์˜ ๊ฐ’์ด select ํ•œ ๊ฒฐ๊ณผ ๊ฐ’์˜ ์ „์ฒด ์ˆ˜๋ฅผ pageSize๋กœ ๋‚˜๋ˆ„์–ด ์˜ฌ๋ฆผํ•œ ๊ฐ’๋ณด๋‹ค ํฐ ๊ฒฝ์šฐ๋Š” null์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค๋‹ˆ๋‹ค.

 

๐Ÿ“‚ Service/artist.js

์ด์ œ Service์—์„œ ๊ณ„์‚ฐํ•œ start์™€ end ๊ฐ’์„ model์—์„œ ๋ฐ›์•„์„œ sql๋ฌธ์„ ์‹คํ–‰ํ•˜๊ณ  ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

exports.pageAll = async (start, end) => {
  try {
   const sql = `SELECT * FROM ARTISTS_TB LIMIT ${start}, ${end}`;
   const result = await pool.queryParam(sql);
   if (result.length === 0) {
      return false;
    } else {
      return result;
    }
  } catch (err) {
    throw err;
  }
};

 

 

๐Ÿƒ‍โ™€๏ธ ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ํ…Œ์ŠคํŠธํ•ด๋ณด๊ธฐ

GET์œผ๋กœ ์„ค์ •ํ•ด์ฃผ๊ณ  localhost:3000/artist?page=1&pageSize=5๋ฅผ ํ•ด์ค๋‹ˆ๋‹ค. ํ˜„์žฌ ๋‹ค๋ฅธ ์ฝ”๋“œ๋กœ ๊ฐœ๋ฐœ ์ค‘์ด์–ด์„œ ์‚ฌ์ง„ ์ƒ url์ด ๋‹ค๋ฅด์ง€๋งŒ ๊ฒฐ๊ณผ๋Š” ๊ฐ™์œผ๋‹ˆ ์œ„ url๋กœ ์ง„ํ–‰ํ•ด์ฃผ์„ธ์š”.

page = 1 ์ผ ๋•Œ ๊ฒฐ๊ณผ 
page = 2 ์ผ ๋•Œ ๊ฒฐ๊ณผ
page = 3 ์ผ ๋•Œ ๊ฒฐ๊ณผ

์ง€๊ธˆ๊นŒ์ง€ node.js์—์„œ offset ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง•์„ ๊ตฌํ˜„ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๐Ÿค” Offset ๋ฐฉ์‹์˜ ๋ฌธ์ œ

offset ๋ฐฉ์‹์€ ์˜ˆ๋ฅผ ๋“ค์–ด LIMIT 4000, 100์ด๋ผ๊ณ  ํ•œ๋‹ค๋ฉด offset ๋ฐฉ์‹์€ 4000๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ์ฝ์€ ๋‹ค์Œ 100๊ฐœ๋ฅผ ์ œํ•œํ•ด์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ๋ฅผ ๋‹ค ๊ฐ€์ง€๊ณ ์™€์„œ ๊ทธ ์•ˆ์—์„œ limit์„ ๊ฑธ์–ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์ „์ฒด ๊ฒŒ์‹œ๊ธ€ ์ˆ˜๋ฅผ ์•Œ๊ธฐ์—๋Š” ํŽธํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ select ํ•ด์˜จ ๊ฒฐ๊ณผ ๊ฐ’์ด ๋งŽ์•„์ง€๋ฉด ๋งŽ์•„์งˆ ์ˆ˜๋ก ์ˆœํšŒํ•˜๋Š” rows ์ˆ˜๊ฐ€ ๋งŽ์•„์ง€๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ์ƒ์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ๋ฐ–์— ์—†์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ๋ฌดํ•œ ์Šคํฌ๋กค์ด๋‚˜ '๋”๋ณด๊ธฐ' ๊ฐ™์€ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” cursor ๊ธฐ๋ฐ˜์˜ ํŽ˜์ด์ง•์œผ๋กœ ๋„˜์–ด๊ฐ€๋Š” ํŽธ์ด ํ›จ์”ฌ ์ข‹์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ๋Š” ๊ทธ๋ž˜์„œ cursor ๋ฐฉ์‹์œผ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๊ฒƒ์„ ๋‹ค๋ค„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!

๐Ÿ“š ์ฐธ๊ณ 

velog.io/@minsangk/%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-Cursor-based-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0