가수면

랜덤한 요소의 개수 계산해 페이지네이션하기 본문

일지

랜덤한 요소의 개수 계산해 페이지네이션하기

니비앙 2023. 8. 29. 20:40

일반적인 페이지네이션 혹은 슬라이드를 구현한다고 한다면 다음과 같은 준비물이 필요할 것이다.

1. 한 번에 몇 개의 요소를 렌더링 시킬 건지 렌더링시킬 요소 개수

2. 전체 배열을 1번으로 나눈 페이지 수

3. slice

 

자, 그렇다면

길이가 제각각인 현재 나의 키워드의 개수가 영역을 넘어가면 어떻게 처리할 것인가..!

 


기능 구현에 있어 가장 큰 문제는 한 페이지에 몇 개가 렌더링되는지가 키워드 길이에 따라서 랜덤하게 결정된다는 것이었다.

 

레퍼런스를 어느 곳에서도 찾을 수 없었기에 나는 차근차근 접근해보았다.

로직 구상

먼저 구현에 있어 가장 중요한 것은 영역 안에 몇 개가 렌더링되는지 카운팅하는 것이다.

overflow hidden을 없앤 모습

현재 로직은 flex-wrap과 overflow hidden 속성을 이용해 레이아웃을 구현한 상태다.

만약 저 초록색 영역 안에 몇 개 요소가 있는지 알 수만 있다면 사실상 구현의 5할 이상은 끝난 것이라 할 수 있겠다.

 

나는 영역 안에 개수를 카운팅 하는 방법으로 크게 두 가지 방법이 생각났다.

1. 영역 안 우측 하단 끝에 있는 맨 마지막 요소를 알아내어 카운팅하는 방법

2. 말 그대로 영역 안에 몇 개가 들어있는지 알아내어 카운팅하는 방법

 

2번은 무슨 개똥같은 소리인가 하겠지만,

 

우습게도 나는 '말 그대로 영역 안에 몇개가 들어있는지 알아내는 방법'에 대한 로직이 먼저 떠올랐다.

 

https://jhchoi1182.tistory.com/183

 

좌표 구하기

offsetLeft / offsetTop 해당 요소가 부모 요소 내에서 위치한 좌표값. 해당 요소의 경계선 왼쪽 위 모서리부터의 거리를 나타냄. clientX / clientY 브라우저 화면에서 마우스 클릭 위치의 좌표값. 브라우

jhchoi1182.tistory.com

 

offsetTop

해당 요소가 부모 요소 내에서 위치한 좌표값.

내가 이전에 공부해 정리해놓은 글을 살펴보면 그렇게 적혀있다.

 

이걸 응용해보면,

부모 요소 내에 위치한 카테고리 요소 좌표값이 부모 요소의 높이보다 크면 영역 아웃이라는 얘기가 될 수 있었다.

 

 

바로 아래 그림처럼 말이다!

영역 안
영역 밖

로직 구현

1. 렌더링된 요소 개수 구하기

먼저 로직으로 실현하기 위해 필요한 작업은 다음과 같았다.

1) 부모 요소에 ref 주기

사실 높이를 200px로 지정해놓은 상태지만 추후 반응형이 적용되었을 때 높이가 바뀔 수도 있기에 높이를 동적으로도 계산할 수 있는 로직이 필요했다.

2) 부모 요소에 relative 속성 부여하기

그래야 부모 요소를 기준으로 offsetTop을 계산할 수 있을 것이다.

3) 페이지 index를 의존성 배열로 갖는 useEffect

페이지가 바뀔 때마다 렌더링된 요소의 개수를 카운트해야하니 필수적인 로직이라고 할 수 있겠다.

 

이후 category 전체 요소를 가져와 영역의 높이와 비교해 작은 것들만 필터링해주는 로직을 만들었다.

  React.useEffect(() => {
    const container = containerRef.current;
    if (container) {
      const totalMyCategories = Array.from(
        container.getElementsByClassName("category"),
      ) as HTMLDivElement[];
      const renderedItems = totalMyCategories.filter(
        (item) => item.offsetTop < container.clientHeight,
      );
      setRenderedItemCount(renderedItems.length);
    }
  }, [data, startIndex, containerRef]);

 

2. 다음 페이지 버튼과 slice만들기

 

mdn의 slice를 간만에 들어가 살펴보니 오히려 생각보다 더 간단한 로직이 가능할 것 같다라는 생각이 들었다.

MDN에서 설명하는 slice

지금 로직 구현에 필요한 것은 몇 개에서 몇 개씩 자르는 게 아니라 앞의 숫자에 렌더링된 요소 숫자만 더해가며 자르면 되는 로직이었기 때문에 숫자 하나만 가지고 slice 로직을 구현하면 될 것 같다.

 

1) 다음 버튼 만들기

slice에 사용될 index라는 상태값을 만들어 앞서 말한 기능을 수행하는 next버튼을 만들었다.

  const handleNext = () => {
    setIndex((prev) => prev + renderedItemCount);
  };

2) slice 만들기

파생값이면 충분하다.

  const slicedMyCategories = data.slice(index);

3)매핑되는 data를 slicedMyCategories로 교체하기

이하 생략

 

3. 마지막 페이지 만들기

이제 마지막 페이지에는 다음 버튼이 안 보여야할 것이다.

마지막 페이지라는 걸 알 수 있는 방법은 무엇일까?

 

현재까지 렌더링된 요소 개수와 원본 배열의 전체 요소 개수가 같다면 그게 마지막 페이지일 것이다.

  const isLastPage = index + renderedItemCount === data.length;

 

4. 이전 버튼 만들기

다음으로 넘어가는 게 된다면 이전으로도 넘어갈 수 있어야한다.

이전으로 넘어가려면 이전에 더 했던 만큼 빼면 된다.

예를 들어 1부터 10까지 배열 중 slice(0)에서 다음을 눌러 slice(3)이 되었다면, 이전 버튼을 눌렀을 때 다시 3을 빼서 slice(0)을 만들면 된다.

이러한 이유로 나는 이전에 렌더링 된 숫자가 담긴 배열을 하나 만들어서 이전 버튼을 누를 때마다 그 배열에서 pop시키는 로직을 만들기로 했다.

그러려면 다음 버튼을 누를 때 렌더링 된 요소를 하나씩 배열 추가하는 로직이 추가되어야할 것이다.

  const handleNext = () => {
    setIndex((prev) => prev + renderedItemCount);
    setRenderedItemCountArray((prev) => [...prev, renderedItemCount]); // 추가
  };

  const handlePrev = () => {
    const lastRenderedItemCount =
      renderedItemCountArray[renderedItemCountArray.length - 1];
    setIndex((prev) => prev - lastRenderedItemCount);
    setRenderedItemCountArray((prev) => {
      prev.pop();
      return prev;
    });
  };

 

5. 첫 페이지 만들기

첫 페이지에서도 마찬가지로 이전 버튼이 보이지 말아야 할 것이다.

첫 페이지를 분기하려면 배열 전체 요소 개수와 배열의 남은 요소 개수가 같으면 그게 첫 페이지일 것이다.

 

1) 남은 개수 세는 상태값 만들기

그리고 내 경우는 카테고리가 실시간 추가/삭제가 가능한 기능이었기에 요소의 남은 개수를 상태값으로 관리해 실시간으로 바뀌는 값을 알아야할 필요가 있었다.

먼저 배열에 추가/삭제가 일어날 때마다 남은 개수를 카운트 하기 위한 useEffect를 만들어주었다.

  const [renderedItemCount, setRenderedItemCount] = React.useState(0);

  const isFirsPage = remainingCategoryCount === data.length;

  React.useEffect(() => {
    setRemainingCategoryCount(slicedMyCategories.length);
  }, [slicedMyCategories]);

 

2) 다음, 이전 버튼에 로직 추가해주기

그리고 이전 버튼과 다음버튼을 누를 때마다 남은 개수를 업데이트하도록 로직을 추가하였다.

  const handleNext = () => {
    setIndex((prev) => prev + renderedItemCount);
    setRemainingCategoryCount((prev) =>			// 추가
      prev > 0 ? prev - renderedItemCount : 0,
    );
    setRenderedItemCountArray((prev) => [...prev, renderedItemCount]);
  };
  
    const handlePrev = () => {
    const lastRenderedItemCount =
      renderedItemCountArray[renderedItemCountArray.length - 1];
    setIndex((prev) => prev - lastRenderedItemCount);
    setRemainingCategoryCount((prev) => prev + lastRenderedItemCount);	// 추가
    setRenderedItemCountArray((prev) => {
      prev.pop();
      return prev;
    });
  };

 

완성~!

커스텀훅으로 분리

마이 페이지에도 같은 기능을 하는 부분이 있었기에, 커스텀훅으로 분리하였다.

아래는 분리한 전체 코드

import React from "react";

const usePagination = (
  data: string[],
  containerRef: React.MutableRefObject<HTMLUListElement | null>,
) => {
  const [remainingCategoryCount, setRemainingCategoryCount] = React.useState(0);
  const [renderedItemCount, setRenderedItemCount] = React.useState(0);
  const [index, setIndex] = React.useState(0);
  const [renderedItemCountArray, setRenderedItemCountArray] = React.useState<
    number[]
  >([]);

  const slicedMyCategories = data.slice(index);
  const isFirsPage = remainingCategoryCount === data.length;
  const isLastPage = index + renderedItemCount === data.length;

  const handleNext = () => {
    setIndex((prev) => prev + renderedItemCount);
    setRemainingCategoryCount((prev) =>
      prev > 0 ? prev - renderedItemCount : 0,
    );
    setRenderedItemCountArray((prev) => [...prev, renderedItemCount]);
  };

  const handlePrev = () => {
    const lastRenderedItemCount =
      renderedItemCountArray[renderedItemCountArray.length - 1];
    setIndex((prev) => prev - lastRenderedItemCount);
    setRemainingCategoryCount((prev) => prev + lastRenderedItemCount);
    setRenderedItemCountArray((prev) => {
      prev.pop();
      return prev;
    });
  };

  React.useEffect(() => {
    const container = containerRef.current;
    if (container) {
      const totalMyCategories = Array.from(
        container.getElementsByClassName("category"),
      ) as HTMLDivElement[];
      const renderedItems = totalMyCategories.filter(
        (item) => item.offsetTop < container.clientHeight,
      );
      setRenderedItemCount(renderedItems.length);
    }
  }, [data, index, containerRef]);

  React.useEffect(() => {
    setRemainingCategoryCount(slicedMyCategories.length);
  }, [slicedMyCategories]);

  return { isFirsPage, isLastPage, handlePrev, handleNext, slicedMyCategories };
};

export default usePagination;
Comments