페이지 만들기 – Pagination
Firestore Database에 있는 컬렉션 문서들을 불러와 화면에 표시하면서, 여러 가지 필드 중에 선택한 필드를 기준으로 오름차순 또는 내림차순으로 정렬하여 순서대로 표시하는 방법을 소개한 바 있었다.
그런데 화면에 표시할 컬렉션 문서들의 수가 5-7개 정도만 넘어도 화면을 꽉 차고, 너무 길어져서 하나의 화면에서는 보기가 어려워진다. 이러한 이유로 여러 문서들을 일정한 개수 단위로 페이지를 다음과 같이 구분하여 표시하는 것이 좋다. 이러한 방법을 Pagination이라고 한다.
파이어베이스에서 Pagination 적용하기
파이어 베이스에서는 이러한 pagination을 .limit(페이지당문서수) 메서드와, startAfter() 메서드를 이용하여 매우 간단하게 구현할 수가 있다.
즉, 미리 페이지당 표시할 문서의 수와, 각각의 마지막으로 표시된 문서를 표시할 변수를 선언해 주고
다음과 같이 미리 준비해둔 정렬 순서 선택 버튼 중 특정 버튼이 눌려지면,
다음과 같이 각각의 버튼에 할당된 아주 간단한 핸들러들에서 정렬 기준 필드와, 정렬 순서 값을 할당해 준후,
다음과 같이 화면 출력 함수를 호출해 값을 표시하는 과정에
다음과 같은 6-7줄의 간단한 코드를 추가하는 것만으로 Pagination 구현이 가능하다.
만일 단순한 페이지 구분 외에, 각각의 페이지에서 이전 페이지와 다음 페이지로 이동하는 버튼과 기능까지 구현하려면 위에서 준비한 form에 다음과 같이 이전 페이지와 다음 페이지 선택을 위한 버튼을 추가해 주는 것과,
해당 버튼이 눌려졌을 때 페이지 이동을 위해 다음과 같은 매우 간단한 핸들러 2개도 추가해 주면 완성되고, 다음과 같이 미리 지정해둔 문서의 개수 단위로 페이지가 구분되는 방식으로 DB 문서들이 출력되며, 이전 페이지와 다음 페이지 버튼을 누르면 각각의 정렬 상태에서 이전 또는 다음 페이지로 이동하게 된다.
Firebase에서 Pagination 적용의 문제점
다만, 이렇게 파이어베이스에 pagination을 적용해보면,
ID 번호순으로 정렬된 다음과 같은 첫페이지에서
<Page 1>
다음 페이지로의 전환을 눌러보면, 다음 화면과 같이 아주 정상적으로 동작한다.
<Page 2>
그런데, 이러한 Page 2에서 “이전 페이지”를 선택해보면 Page번호는 1로 변경이 되지만, 실제 표시되는 레코드의 ID는 위에서 제시한 Page1과 다르게 표시되는 현상이 발견된다.
<이전페이지 버튼으로 이동한 Page 1>
이에 대해 여러 자료들을 찾아본 결과, 파이어베이스는 Query시 순차적으로 데이타를 읽기 때문에, 다음 레코드를 읽어 가져오는 것은 문제가 없지만, 이전 데이타로 다시 돌아가는데 근원적인 문제가 있다고 한다.
만일 이러한 문제를 해결해서 이전페이지로의 이동까지 완벽하게 동작하게 하려면, 모든 데이타를 배열형태의 데이타로 메모리로 읽어와 처리해야 한다는 글을 보았다. 그런데 이런 방법은 레코드 수가 많아지면 감당하기 어려워지는 문제가 있어서 어떻게 구현할지를 고민해 보게 되었다.
우선 당장은 “이전 페이지” 버튼은 잠시 비활성화시켜두고 “다음 페이지”만 활용해야 할 듯 하다.
소스 코드
위의 사항들을 반영하여 작성한 전체 소스 코드는 다음과 같다. (이전 페이지 선택버튼은 일단 비활성화시켜둠)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>데이타 읽기</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<style>
.card-body {
padding-top: 0.1rem !important; /* 원하는 간격으로 조정하세요 */
}
.card-text {
margin-bottom: 0.01rem !important; /* 원하는 간격으로 조정하세요 */
}
</style>
<script src="https://www.gstatic.com/firebasejs/8.6.5/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.5/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.5/firebase-firestore.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.5/firebase-storage.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="/firebaseConfig.js"></script>
</head>
<body>
<div class="container">
<div class="m-3">
<button id="sortByRegNumAsc" class="btn btn-sm btn-outline-primary">번호↑</button>
<button id="sortByRegNumDesc" class="btn btn-sm btn-outline-primary">번호↓</button>
<button id="sortByNameAsc" class="btn btn-sm btn-outline-success">상품명 ↑</button>
<button id="sortByNameDesc" class="btn btn-sm btn-outline-success">상품명↓</button>
<button id="sortByPriceAsc" class="btn btn-sm btn-outline-danger">가격↑</button>
<button id="sortByPriceDesc" class="btn btn-sm btn-outline-danger">가격↓</button>
<button id="sortByDateAsc" class="btn btn-sm btn-outline-secondary">등록일 ↑</button>
<button id="sortByDateDesc" class="btn btn-sm btn-outline-secondary">등록일 ↓</button>
</div>
<div id="productList" class="m-3"></div>
<div class="m-3">
<!-- <button id="prevPage" class="btn btn-sm btn-outline-danger">이전 페이지</button> -->
<span id="currentPage" style="margin: 70px 70px;"></span>
<button id="nextPage" class="btn btn-sm btn-outline-danger">다음 페이지</button>
</div>
</div>
<script>
var db = firebase.firestore();
var storage = firebase.storage();
var pageSize = 4; // 한 페이지에 표시할 항목 수
var lastVisible; // 마지막으로 표시된 항목
var currentPage = 1; // 현재 페이지 번호
var currentField = "상품등록번호"; // 현재 정렬 필드
var currentDirection = "asc"; // 현재 정렬 방향
function loadProducts(orderBy, orderDir) {
$('#productList').empty();
var query = db.collection("products").orderBy(orderBy, orderDir).limit(pageSize);
if (lastVisible) {
query = query.startAfter(lastVisible);
}
query.get().then((querySnapshot) => {
if (!querySnapshot.empty) {
lastVisible = querySnapshot.docs[querySnapshot.docs.length-1];
}
querySnapshot.forEach((doc) => {
var data = doc.data();
var date = new Date(data.날짜.seconds * 1000).toLocaleString();
var html = `
<div class="card mb-6" style="max-width: 540px;">
<div class="row g-0">
<div class="col-md-4">
<img src="${data.사진}" alt="${data.상품명}" style="width: 180px; height: 180px;">
</div>
<div class="col-md-7">
<div class="card-body">
<p class="card-text"><small class="text-muted">ID : ${data.상품등록번호}</small></p>
<h7 class="card-title" style="color: red;">${data.상품명}</h7>
<p class="card-text">${data.상품설명}</p>
<p class="card-text"><small class="text-muted">${data.가격}</small></p>
<p class="card-text"><small class="text-muted">${date}</small></p>
<p class="card-text"><small class="text-muted">Note: ${data.참고노트}</small></p>
<p class="card-text"><small class="text-muted">♥ : ${data.좋아요}</small></p>
</div>
</div>
</div>
</div>
`;
$('#productList').append(html);
});
$('#currentPage').text('Page ' + currentPage); // 현재 페이지 번호 업데이트
});
};
$('#nextPage').click(function() {
currentPage++;
loadProducts(currentField, currentDirection);
});
$('#prevPage').click(function() {
if (currentPage > 1) {
currentPage--;
loadProducts(currentField, currentDirection);
}
});
$(document).ready(function() {
loadProducts(currentField, currentDirection);
$('#sortByRegNumAsc').click(function() {
resetPagination();
currentField = "상품등록번호";
currentDirection = "asc";
loadProducts(currentField, currentDirection);
});
$('#sortByRegNumDesc').click(function() {
resetPagination();
currentField = "상품등록번호";
currentDirection = "desc";
loadProducts(currentField, currentDirection);
});
$('#sortByNameAsc').click(function() {
resetPagination();
currentField = "상품명";
currentDirection = "asc";
loadProducts(currentField, currentDirection);
});
$('#sortByNameDesc').click(function() {
resetPagination();
currentField = "상품명";
currentDirection = "desc";
loadProducts(currentField, currentDirection);
});
$('#sortByPriceAsc').click(function() {
resetPagination();
currentField = "가격";
currentDirection = "asc";
loadProducts(currentField, currentDirection);
});
$('#sortByPriceDesc').click(function() {
resetPagination();
currentField = "가격";
currentDirection = "desc";
loadProducts(currentField, currentDirection);
});
$('#sortByDateAsc').click(function() {
resetPagination();
currentField = "날짜";
currentDirection = "asc";
loadProducts(currentField, currentDirection);
});
$('#sortByDateDesc').click(function() {
resetPagination();
currentField = "날짜";
currentDirection = "desc";
loadProducts(currentField, currentDirection);
});
});
function resetPagination() {
lastVisible = null;
currentPage = 1;
}
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>
