바닐라 자바스크립트 페이지네이션 만들기

요들레이후·2023년 4월 29일
1

프로젝트

목록 보기
2/5
post-thumbnail

1차 프로젝트를 진행하며 바닐라 자바스크립트로 웹페이지를 구현했어야 했다.
우리의 웹페이지는 총 90개 정도의 데이터를 렌더링했어야했기에
페이지네이션이 필수로 구현되어야만 했다.

그전에 새로 알게된 함수에 대해 설명하도록 하겠다.


URLSearchParams

javascript에서 url의 쿼리 파라미터들을 읽거나 수정할 때 사용하는 URLSearchParams 사용법이다.
http://kdt-sw-4-team08.elicecoding.com/?page=5 에서의 결과값

  • location.search
// 현재 위치한 endpoint뒤에 params를 리턴한다.
const params = location.search;
console.log(params); // ?page=5
  • URLSearchParams
    • urlSearchParams.get('paramName')은 해당 paramName으로 조회되는 첫번째 값을 return한다.
    • urlSearchParams.getAll("paramName")paramName으로 조회되는 모든 값을 배열로 return한다.
// URLSearchParams 객체로 변환하여 param의 키값을 이용해 해당 값을 불러올 수 있다. 
const param = new URLSearchParams(params);
const page = param.get('page');  // 5
  • urlSearchParams.set("paramName", "value")를 사용하면, paramName의 value를 변경할 수 있다.
    만약 존재하지 않는 parameterName이라면 append된다.
    만약 parameterName으로 중복되는 값이 있을 경우 1개만 변경된 값으로 남기고 모두 제거된다.
let urlParams = new URLSearchParams("?hayeong=test&log=yes");
urlParams.set("hayeong", "cute");
urlParams.set("log", "good");
console.log(urlParams);
// ?hayeong=cute?log=good

대충 이정도로 개념 설명 마무리하고, 구현한 코드를 보자.


우선 홈화면 렌더링 방식은 동적으로 DOM요소를 생성하며 데이터를 넣어주고 있다.

....

function createCard(product) {
  return `
  <div id="card" style="width:350px; height:480px;" class="mb-5">
    <a id="card-link" href="/product-detail?pid=${product.shortId}">
      <img
        src="${product.productImage}"
        id="card-img-top"
        class="rounded-lg"
        style="width:350px; height:376px;"
      />
      <div id="card-body">
        <div id="card-text card-title" class="text-lg mt-3 mb-1">
          ${product.productName}
        </div>
        <div id="card-text card-hashtags" class="text-gray-500 mt-1 mb-1">
          #${product.category}
        </div>
        <div id="card-text card-price" class="font-bold text-base mt-1 mb-1">
          ${product.productPrice} 원
        </div>
      </div>
    </a>
  </div>`;
}


getProductList();

위는 동적으로 생성할 카드 컴포넌트 1개이다.


아래는 전체 상품을 조회하고 페이지네이션을 하는 함수이다.

/********************* 전체상품 조회 && 페이지네이션 **********************/
// 현재 url의 쿼리 파라미터를 가져와서 URLSearchParams 객체로 변환한다. ex) ?page=5
const urlParams = new URLSearchParams(window.location.search);
// get메서드로 page키의 값을 가져온다. 있으면 해당 값을 page변수에 저장하고, 없으면 1페이지만 존재한다는 것이므로 1로 저장한다.
let page = parseInt(urlParams.get('page')) || 1;

async function getProductList() {
  // axios로 데이터를 받아오고 있다. param으로 현재 페이지 그리고 한 페이지에 보여질 데이터 수를 넘겨준다.
  const response = await axios.get(`/api/products?page=${page}&perPage=9`);
  const products = await response.data.pagenatedProducts.results;
  const productCount = await response.data.total;

  // #item-cards-list는 동적으로 카드를 생성할 container부분이다.
  // querySelector로 컨테이너 요소를 잡아준다.
  const cards = document.querySelector('#item-cards-list');
  
  // 받아온 상품 데이터를 한 페이지 당 보여질 개수로 나눠주면 총 생성할 페이지가 정의된다.
  const totalPages = Math.ceil(productCount / 9);
  productCounter.innerText = productCount;

  // 페이지 버튼을 만들 컨테이너 요소
  const pageButtons = document.querySelector('#page-buttons');
  // 위의 버튼 컨테이너를 제일 먼저 초기화시켜준다. -> 전체상품만 페이지네이션하는 것이 아니고 카테고리별로도 페이지네이션을 구현하기 때문
  pageButtons.innerHTML = '';
  // 위에서 정의한 총 페이지 수만큼 link태그를 생성하고 classList.add로 tailwind css를 입힌다.
  for (let i = 1; i <= totalPages; i++) {
    const link = document.createElement('a');
    link.classList.add('mt-20', 'mb-10', 'mr-10', 'text-xl');
    // 패이별로 url href params를 붙여준다.
    link.href = `?page=${i}`;
    // 1,2,3, 4, 5... 버튼에 나타날 숫자
    link.textContent = i;
    // 현재 페이지를 누르고 있다면 강조 효과 css
    if (i === page) {
      link.classList.add('text-[#69b766]', 'font-bold');
    }
    // 하나씩 pageButton요소에 추가한다.
    pageButtons.appendChild(link);
  }

  // product 각 요소마다 createCard함수 호출하여 productList에 담음
  const productList = [];
  for (const product of products) {
    const newCard = createCard(product);
    cards.innerHTML += newCard;
    productList.push(product);
  }
  return productList;
}

카테고리별로 페이지네이션하는 것도 동일하지만, 조금 더 url에 신경써줘야한다.
예를 들어 카테고리를 눌렀을 시에 전에 있던 url을 새롭게 변경해야하는 작업이 필요하다.
따라서 나는 아래와 같이 구현했다.

...

// 카테고리 클릭시 url업데이트
function updateUrl(categoryPage) {
  const clickedCategoryName = sessionStorage.getItem('selectedCategory');
  const newUrl = `?category=${clickedCategoryName}&categoryPage=${categoryPage}`;
  window.history.pushState(null, null, newUrl);
}

...
async function categoryFilter() {
  const clickedCategoryName = sessionStorage.getItem('selectedCategory');
  const searchByCategoryProductList = [];
  let products = [];
  try {
    const response = await axios.get(
      `/api/products/categories?category=${clickedCategoryName}&page=${categoryPage}&perPage=9`,
    );

    const cards = document.querySelector('#item-cards-list');

    products = await response.data.pagenatedProducts.results;
    const productCount = await response.data.total;
    const totalPages = Math.ceil(productCount / 9);

    productCounter.innerText = productCount;

    // 똑같이 pageBtn 동적 생성해주는 코드이다.
    const pageButtons = document.querySelector('#page-buttons');
    pageButtons.innerHTML = '';
    for (let i = 1; i <= totalPages; i++) {
      const link = document.createElement('a');
      link.classList.add('mt-20', 'mb-10', 'mr-10', 'text-xl');
      link.href = `?category=${clickedCategoryName}&categoryPage=${i}`;

      link.textContent = i;

      if (i === categoryPage) {
        link.classList.add('text-[#69b766]', 'font-bold');
      }

      // 여기서 다른 부분이다. link.addEvenListener로 link를 클릭할 때 마다 카테고리 페이지를 다시 만들어주는 작업을 했다. 
      //이렇게 하지 않으면 각 카테고리에서 다른 페이지번호를 누를 때 전체페이지로 넘어가는 기이한 현상이 발생한다.
      // 캬테고리 페이지를 현재 페이지로 지정해주고 url을 현재 카테고리 페이지로 업데이트 시킨다음, categoryFilter함수를 다시 재호출해 해당하는 url로 get 요청을 다시 보내준다.
      
      link.addEventListener('click', (e) => {
        e.preventDefault();
        categoryPage = i;
        updateUrl(categoryPage);
        categoryFilter();
      });

      pageButtons.appendChild(link);
    }

    products.forEach((product) => {
      searchByCategoryProductList.push(product);
    });

    if (searchByCategoryProductList.length === 0) {
      productCounter.innerText = 0;
      cards.innerHTML = `
    <div class="col-start-1 col-end-4 pt-[150px] pb-[170px]" style="margin:0 auto">
      <div class="col-start-1 col-end-4 text-2xl font-semibold text-gray-500" id="empty-product-list" >상품이 없습니다.</div>
    </div>
    `;
    } else {
      cards.innerHTML = '';
      searchByCategoryProductList.forEach((product) => {
        const newCard = createCard(product);
        cards.innerHTML += newCard;
      });
    }
  } catch (err) {
    const spinner = document.getElementById('spinner');
    spinner.innerHTML = `
      <section
      id="item-cards-list"
      class="grid grid-cols-3 justify-items-center items-center mb-50"
      style="width: 1200px"
    ></section>
  `;

    const cards = document.querySelector('#item-cards-list');
    cards.innerHTML = `
    <div class="col-start-1 col-end-4 pt-[150px] pb-[170px]" style="margin:0 auto">
      <div class="col-start-1 col-end-4 text-2xl font-semibold text-gray-500" id="empty-product-list" >상품이 없습니다.</div>
    </div>
    `;
    const pageButtons = document.querySelector('#page-buttons');
    pageButtons.innerHTML = '';
    productCounter.innerText = 0;
  }
}
profile
성공 = 무한도전 + 무한실패

0개의 댓글