Vue.js 3 & NodeJS/사내교육 - 점심 메뉴

[4] Backend 개발 - API Server 개발, Node.JS + Paging

carrotweb 2026. 8. 3. 14:59
728x90
반응형

페이징(Paging), 페이지(Page)

데이터베이스에서 페이징(Paging) 없이 데이터를 가져오면 전체 데이터를 반환하면 서버 부하(데이터베이스 부하, 네트워크 트래픽 부하)가 발생합니다. 그래서 데이터를 가져오기 전에 한 번에 가져올 데이터의 크기를 정해서 가져오게 해야 합니다.

이렇게 일정한 크기로 데이터를 가져오는 것을 페이징(Paging)이라고 하고 일정한 크기를 가진 데이터를 페이지(Page)라고 합니다.

 

 

Query String(쿼리 스트링)으로 URL 주소 뒤에 붙여서 사용합니다.

  • 페이지 크기(countperpage) : 가져올 ROW 수
  • 페이지 번호(pageno) : 가져올 페이지 번호

 

http://127.0.0.1:9000/api/v1/lunchmenus?countperpage=10&pageno=1
1번부터 10번까지 점심 메뉴를 가져옵니다.

 

http://127.0.0.1:9000/api/v1/lunchmenus?countperpage=5&pageno=2
6번부터 10번까지 점심 메뉴를 가져옵니다.

 

 

페이징(Paging) 기능 추가

1. src\api\v1\lunchmenu.js 파일을 수정합니다.

점심 메뉴 조회에 페이지 크기(countperpage), 페이지 번호(pageno)를 추가합니다.

// 전체 점심 메뉴 리스트를 리턴합니다.
router.get('/', async function(req, res, next) {
    console.log("REST API Get Method - Read All Lunch Menu.");

    // 페이지 크기
    let countPerPage = req.query.countperpage;
    // 페이지 번호
    let pageNo = req.query.pageno;

    // 페이지 크기가 없으면 10으로 설정
    if (countPerPage == undefined || typeof countPerPage == "undefined" || countPerPage == null) {
        countPerPage = 10;
    } else {
        countPerPage = parseInt(countPerPage);
    }
    // 페이지 번호가 없으면 1로 설정
    if (pageNo == undefined || typeof pageNo == "undefined" || pageNo == null) {
        pageNo = 1;
    } else {
        pageNo = parseInt(pageNo);
    }

    :
}

 

2. src\db\mappers\mariadb\lunchmenu\lunchmenu-sql.xml 파일을 수정합니다.

페이지 크기(countperpage)와 페이지 번호(pageno)를 사용하여 로우 시작 번호와 끝 번호를 계산하기 위해서 전체 크기를 가져와야 합니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="apiserver.db.mappers.mariadb.lunchmenu">  
    <select id="selectLunchMenuList">
        SELECT *
          FROM tb_lunch_menu
         ORDER BY REG_DTM DESC
    </select>

    <select id="selectLunchMenuListCount">
        SELECT count(seq) AS listCount
          FROM tb_lunch_menu
    </select>

    :

 

 

쿼리문에 페이징(Paging)을 처리하기 위해 LIMIT를 추가합니다. (https://mariadb.com/kb/en/limit/)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="apiserver.db.mappers.mariadb.lunchmenu">  
    <select id="selectLunchMenuList">
        SELECT *
          FROM tb_lunch_menu
         ORDER BY REG_DTM DESC
         LIMIT ${pageStartRecordNo}, ${countPerPage}
    </select>

    :

ROW 시작 번호부터 가져올 ROW 수만큼 ROW를 가져옵니다.

 

Oracle에서는 ROW_NUMBER를 사용하여 ROW의 번호를 부여하고 WHERE 절에서 BETWEEN으로 가져옵니다.

<select id="selectLunchMenuList">
    SELECT   *
              , RNUM
     FROM (
               SELECT   *
                         , ROW_NUMBER() OVER(ORDER BY REG_DTM) AS RNUM
                FROM tb_lunch_menu
              )
    WHERE RNUM BETWEEN ${pageStartRecordNo} AND ${pageEndRecordNo}
</select>

 

3. src\api\v1\lunchmenu.js 파일을 수정합니다.

페이지 크기(countperpage)와 페이지 번호(pageno)를 사용하여 로우 시작 번호와 끝 번호를 계산합니다.

// 전체 점심 메뉴 리스트를 리턴합니다.
router.get('/', async function(req, res, next) {
    :

    let conn;
    try {
        conn = await getDBConnection();

        //const query = "SELECT count(seq) AS listCount FROM tb_lunch_menu";
        let query = mybatisMapper.getStatement('apiserver.db.mappers.mariadb.lunchmenu', 
            'selectLunchMenuListCount', {}, format);

        // 전체 크기
        const dataCount = await conn.query(query);
        if (dataCount.length == 0) {
            result.message = "데이터가 없습니다.";
        } else {
            // 결과에 숫자 타입(int(n, N), float(f, F), double(d, D), long(l, L))이 같이 옵니다. [ { listCount: 3n } ]
            const totalCount = parseInt(dataCount[0].listCount);

            // 시작 번호
            let pageStartRecordNo = ((pageNo - 1) * countPerPage);
            // 전체 크기가 0이면
            if (totalCount == 0) {
                pageStartRecordNo = 0;
            }
            // 종료 번호
            let pageEndRecordNo = (pageNo * countPerPage) - 1;
            // 전체 크기가 0이면
            if (totalCount == 0) {
                pageEndRecordNo = 0;
            } else if (pageEndRecordNo > (totalCount - 1)) {
                // 종료 번호가 전체 크기보다 크면 전체 크기로 변경
                pageEndRecordNo = totalCount - 1;
            }

            const params = {
                pageStartRecordNo : pageStartRecordNo,
                countPerPage : countPerPage
            };

            //const query = "SELECT * FROM tb_lunch_menu ORDER BY REG_DTM DESC";
            query = mybatisMapper.getStatement('apiserver.db.mappers.mariadb.lunchmenu', 
                'selectLunchMenuList', params, format);

            const data = await conn.query(query);
            if (data.length == 0) {
                result.message = "데이터가 없습니다.";
            } else {
                result.message = "데이터가 조회되었습니다.";
                result.data = data;
            }
        }
    }

    :

 

 

점심 메뉴 API 테스트 (POSTMAN에서 확인)

1. 전체 점심 메뉴 조회

 

 

페이지네이션(Pagination)

나누어진 Page(페이지)를 이전 페이지나 다음 페이지 또는 특정 페이지로 이동할 수 있게 처리하는 것을 Pagination(페이지네이션)이라고 합니다.

 

 

  • 페이지 사이즈(pageSize)
  • 전체 마지막 페이지 번호(lastPageNo)
  • 시작 페이지 번호(startPageNo)
  • 종료 페이지 번호(endPageNo)
  • 이전 페이지 번호 = 시작 페이지 번호(startPageNo) - 1
  • 다음 페이지 번호 = 종료 페이지 번호(endPageNo) + 1

 

페이지네이션(Pagination) 정보 추가

1. src\api\v1\lunchmenu.js 파일을 수정합니다.

페이지 사이즈(pagesize)를 추가합니다.

// 전체 점심 메뉴 리스트를 리턴합니다.
router.get('/', async function(req, res, next) {
    console.log("REST API Get Method - Read All Lunch Menu.");

    :
    // 페이지 사이즈
    let pageSize = req.query.pagesize;

    :
    // 페이지 사이즈가 없으면 10으로 설정
    if (pageSize == undefined || typeof pageSize == "undefined" || pageSize == null) {
        pageSize = 10;
    } else {
        pageSize = parseInt(pageSize);
    }

    :
}

 

Query String(쿼리 스트링)으로 URL 주소 뒤에 붙여서 사용합니다.

http://127.0.0.1:9000/api/v1/lunchmenus?pagesize=5&countperpage=5&pageno=3
11번부터 15번까지 점심 메뉴를 가져옵니다.

 

전체 마지막 페이지 번호(lastPageNo)를 계산합니다. 전체 크기(totalCount)를 페이지 크기(countPerPage)로 나누고 나머지가 있으면 1을 더합니다.

// 전체 점심 메뉴 리스트를 리턴합니다.
router.get('/', async function(req, res, next) {
    console.log("REST API Get Method - Read All Lunch Menu.");

    :
        // 전체 크기
        const dataCount = await conn.query(query);
        if (dataCount.length == 0) {
            result.message = "데이터가 없습니다.";
        } else {
            // 결과에 숫자 타입(int(n, N), float(f, F), double(d, D), long(l, L))이 같이 옵니다. [ { listCount: 3n } ]
            const totalCount = parseInt(dataCount[0].listCount);

            // 전체 마지막 페이지 번호
            const lastPageNo = Math.floor(totalCount / countPerPage) + (totalCount % countPerPage == 0 ? 0 : 1);

    :
}

 

시작 페이지 번호(startPageNo)를 계산합니다.

페이지 사이즈(pageSize)에서 페이지 번호(pageNo)를 나누고 나머지가 없으면 1을 뺍니다.

// 전체 점심 메뉴 리스트를 리턴합니다.
router.get('/', async function(req, res, next) {
    console.log("REST API Get Method - Read All Lunch Menu.");

    :

            // 결과에 숫자 타입(int(n, N), float(f, F), double(d, D), long(l, L))이 같이 옵니다. [ { listCount: 3n } ]
            const totalCount = parseInt(dataCount[0].listCount);

            // 전체 마지막 페이지 번호
            const lastPageNo = Math.floor(totalCount / countPerPage) + (totalCount % countPerPage == 0 ? 0 : 1);
            // 시작 페이지 번호
            let startPageNo = 1;
            // 페이지 사이즈로 페이지 번호를 나눈 몫만큼 페이지 시작 번호 변경
            let start = Math.floor(pageNo / pageSize);
            if (start >= 1) {
                // 그렇지만 나머지가 없으면 현재 페이지 번호가 마지막 페이지 번호와 같아 감소
                if (pageNo % pageSize == 0) {
                    start--;
                }
                startPageNo = (start * pageSize) + 1;
            }
    :
}

페이지 번호가 10이면 start가 1이 됩니다. 그러면 시작 페이지 번호가 11이 됩니다. 그래서 페이지 사이즈의 배수(즉 나머지가 없으면)이면 start에서 1을 뺍니다. 그러면 시작 페이지 번호가 1이 됩니다. 

 

종료 페이지 번호(endPageNo)를 계산합니다.

시작 페이지 번호(startPageNo)에서 1을 빼고 페이지 사이즈(pageSize)를 더하면 됩니다.

// 전체 점심 메뉴 리스트를 리턴합니다.
router.get('/', async function(req, res, next) {
    console.log("REST API Get Method - Read All Lunch Menu.");

    :

            // 시작 페이지 번호
            let startPageNo = 1;
            :
            // 종료 페이지 번호
            let endPageNo = (startPageNo - 1) + pageSize;
            // 그렇지만 종료 페이지 번호가 마지막 페이지 번호보다 크면 마지막 페이지 번호로 변경
            if (endPageNo > lastPageNo) {
                endPageNo = lastPageNo;
            }
    :
}

 

페이지네이션(endPageNo)을 추가합니다.

// 전체 점심 메뉴 리스트를 리턴합니다.
router.get('/', async function(req, res, next) {
    console.log("REST API Get Method - Read All Lunch Menu.");

    :

    const result = {
        result: 'success',
        code: 200,
        message: '',
        data: [],
        pagination: {}
    };

            :

            // 페이지네이션 정보
            result.pagination.totalCount = totalCount;
            result.pagination.countPerPage = countPerPage;
            result.pagination.pageSize = pageSize;
            result.pagination.startPageNo = startPageNo;
            result.pagination.endPageNo = endPageNo;
            result.pagination.lastPageNo = lastPageNo;
            result.pagination.pageNo = pageNo;
            :
}

 

 

점심 메뉴 API 테스트 (POSTMAN에서 확인)

1. 전체 점심 메뉴 조회

{
    "result": "success",
    "code": 200,
    "message": "데이터가 조회되었습니다.",
    "data": [
        {
            "seq": 1,
            "menu_name": "짜장면",
            "reg_dtm": "2025-01-17 13:37:00"
        }
    ],
    "pagination": {
        "totalCount": 3,
        "countPerPage": 2,
        "pageSize": 10,
        "startPageNo": 1,
        "endPageNo": 2,
        "lastPageNo": 2,
        "pageNo": 2
    }
}

 

 

페이지네이션(Pagination) 모듈로 처리

1. src\api\v1\pagination.js 파일을 추가합니다.

// 페이지네이션
// 전체 레코드 수 totalCount
// 페이지 번호 pageNo
// 페이지 크기 countPerPage
// 페이지 사이즈 pageSize
const pagination = function(totalCount, pageNo, countPerPage, pageSize) {
    // 페이지 번호가 없으면 1로 설정
    if (pageNo == undefined || typeof pageNo == "undefined" || pageNo == null) {
        pageNo = 1;
    } else {
        pageNo = parseInt(pageNo);
    }
    // 페이지 크기가 없으면 10으로 설정
    if (countPerPage == undefined || typeof countPerPage == "undefined" || countPerPage == null) {
        countPerPage = 10;
    } else {
        countPerPage = parseInt(countPerPage);
    }
    // 페이지 사이즈가 없으면 10으로 설정
    if (pageSize == undefined || typeof pageSize == "undefined" || pageSize == null) {
        pageSize = 10;
    } else {
        pageSize = parseInt(pageSize);
    }

    // 전체 마지막 페이지 번호
    const lastPageNo = Math.floor(totalCount / countPerPage) + (totalCount % countPerPage == 0 ? 0 : 1);

    // 시작 페이지 번호
    let startPageNo = 1;
    // 페이지 사이즈로 페이지 번호를 나눈 몫만큼 페이지 시작 번호 변경
    let start = Math.floor(pageNo / pageSize);
    if (start >= 1) {
        // 그렇지만 나머지가 없으면 현재 페이지 번호가 마지막 페이지 번호와 같아 감소
        if (pageNo % pageSize == 0) {
            start--;
        }
        startPageNo = (start * pageSize) + 1;
    }

    // 종료 페이지 번호
    let endPageNo = (startPageNo - 1) + pageSize;
    // 그렇지만 종료 페이지 번호가 마지막 페이지 번호보다 크면 마지막 페이지 번호로 변경
    if (endPageNo > lastPageNo) {
         endPageNo = lastPageNo;
    }

    // 시작 번호
    let pageStartRecordNo = ((pageNo - 1) * countPerPage);
    // 전체 크기가 0이면
    if (totalCount == 0) {
        pageStartRecordNo = 0;
    }
    
    // 종료 번호
    let pageEndRecordNo = (pageNo * countPerPage) - 1;
    // 전체 크기가 0이면
    if (totalCount == 0) {
        pageEndRecordNo = 0;
    } else if (pageEndRecordNo > (totalCount - 1)) {
        // 종료 번호가 전체 크기보다 크면 전체 크기로 변경
         pageEndRecordNo = totalCount - 1;
    }

    // 페이지네이션 정보
    const paginationInfo = {};
    paginationInfo.totalCount = totalCount;
    paginationInfo.countPerPage = countPerPage;
    paginationInfo.pageSize = pageSize;
    paginationInfo.startPageNo = startPageNo;
    paginationInfo.endPageNo = endPageNo;
    paginationInfo.lastPageNo = lastPageNo;
    paginationInfo.pageNo = pageNo;
    paginationInfo.pageStartRecordNo = pageStartRecordNo;
    paginationInfo.pageEndRecordNo = pageEndRecordNo;
    
    return paginationInfo;
}

module.exports = pagination;

 

2. src\api\v1\lunchmenu.js 파일을 수정합니다.

:
const pagination = require('./pagination');
:

// 전체 점심 메뉴 리스트를 리턴합니다.
router.get('/', async function(req, res, next) {
    console.log("REST API Get Method - Read All Lunch Menu.");

    const result = {
        result: 'success',
        code: 200,
        message: '',
        data: [],
        pagination: {}
    };

    let conn;
    try {
        conn = await getDBConnection();

        //const query = "SELECT count(seq) AS listCount FROM tb_lunch_menu";
        let query = mybatisMapper.getStatement('apiserver.db.mappers.mariadb.lunchmenu', 
            'selectLunchMenuListCount', {}, format);

        // 전체 크기
        const dataCount = await conn.query(query);
        if (dataCount.length == 0) {
            result.message = "데이터가 없습니다.";
        } else {
            // 결과에 숫자 타입(int(n, N), float(f, F), double(d, D), long(l, L))이 같이 옵니다. [ { listCount: 3n } ]
            const totalCount = parseInt(dataCount[0].listCount);

            // 페이지네이션 정보
            // 전체 레코드 수 totalCount
            // 페이지 크기 req.query.countperpage
            // 페이지 번호 req.query.pageno
            // 페이지 사이즈 req.query.pagesize
            const paginationInfo = pagination(totalCount, req.query.pageno, req.query.countperpage, req.query.pagesize);

            if (totalCount > 0) {
                const params = {
                    pageStartRecordNo : paginationInfo.pageStartRecordNo,
                    countPerPage : paginationInfo.countPerPage
                };

                //const query = "SELECT * FROM tb_lunch_menu ORDER BY REG_DTM DESC";
                query = mybatisMapper.getStatement('apiserver.db.mappers.mariadb.lunchmenu', 
                    'selectLunchMenuList', params, format);
                console.log(query);
                const data = await conn.query(query);
                if (data.length == 0) {
                    result.message = "데이터가 없습니다.";
                } else {
                    result.message = "데이터가 조회되었습니다.";
                    result.data = data;
                }
            } else {
                result.message = "데이터가 없습니다.";
            }

            // 페이지네이션 정보
            result.pagination = paginationInfo;
        }
    } catch (error) {
        result.result = "fail";
        result.code = 500;
        if (error.code == "ER_NO_SUCH_TABLE") {
            result.message = "테이블이 없습니다.";
        } else {
            result.message = "서버 오류가 발생하였습니다.";
        }
        console.log(error);
    } finally {
        if (conn) {
            conn.release();
        }
    }

    res.status(result.code).json(result);
});

 

 

728x90
반응형