potato-hyun
감자현 | potato-hyun
potato-hyun
전체 방문자
오늘
어제
  • 분류 전체보기
    • Life
    • Dev
      • 코딩공부 이모저모
      • CS, 자료구조, 알고리즘
      • 코딩테스트 문제 풀이
      • 트러블슈팅, 오류해결록
    • Language
      • Javascript
    • JP
    • Learning
      • bookclub

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 코딩뉴비
  • CS50
  • edwith
  • 비욘더로드
  • 미쓰잭슨
  • 쉐도잉
  • 주입
  • node-schedule
  • prettier
  • ubutu
  • nestjs
  • vscode
  • 코딩 #개발자 #노마드북클럽 #노개북
  • 스크립트
  • ncloud
  • beautify
  • Beyond The Road
  • 상태코드
  • http
  • 퍼블리싱
  • 네이버클라우드
  • 이벤트리스너
  • Status Code
  • restful api
  • 의존성
  • 부스트코스
  • 보노보노
  • nodejs
  • 더현대서울
  • 오토스케일링

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
potato-hyun

감자현 | potato-hyun

Node.js ) 페이징 처리 1 - offset, limit 방식
Dev/코딩공부 이모저모

Node.js ) 페이징 처리 1 - offset, limit 방식

2021. 7. 24. 18:30

목차

     

    백앤드 공부하면 빼놓을 수 없는 개념, 바로 페이징!

    처음에 페이징 처리를 배웠을때 나름 중-고급이라는 교수님말에 지레 겁먹은 기억이 있다.

    아니... 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 ;

     

    (좌) limit 0, 5 / (우) 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}>&laquo;</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}>&raquo;</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방식으로! 고고고🏌️‍♀️

    +) 현재 페이지 쿼리 스트링과 연결할 예정!

     

     

    참고 블로그

    https://gocoder.tistory.com/1064

    저작자표시 (새창열림)

    '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
      'Dev/코딩공부 이모저모' 카테고리의 다른 글
      • CS) 서버 OS 알아보기, CentOS, Linux, Ubuntu, Window
      • CDN ) CDN 결과 비교하기, 근데 거기에 Request header 분석을 곁들인...
      • Node.js ) Storage 에서 사용하지 않는 더미 파일들 정리하기 -- ft.서버 스케줄링
      • React Router ) Link 기능을 함수로 빼기, history와 location
      potato-hyun
      potato-hyun
      말하는 감자

      티스토리툴바