[TIL] React Pagination 구현에 대한 성찰

Dico·2021년 8월 8일
9

[TIL]

목록 보기
12/13

난생 처음 8명이서 한 팀이 되어 과제를 진행했다.

'이렇게 많은 인원이 어떻게 누구 하나 소외되는 사람 없이 각자의 역할을 제대로 해낼 수 있게 분업을 할 수가 있을까?'
라는 고민이 있었지만, 페이지 별로, 혹은 기능별로 분업이 순조롭게 이어졌다.

스스로 로직을 고민하는 경험이 필요하다고 느껴오고 있었던만큼, 나는 이번에 어드민 페이지의 아이디들을 페이지별로 불어오는 페이지네이션(Pagination)을 맡겠다고 했다.


설계 / 구현순서

페이지네이션을 구현하려면 무엇을 상태로써 관리해야 할까?
대략적으로 구상해본 구현순서는 아래와 같다:

  • 테이블에 들어갈 전체 사용자 데이터를 받아온다.
  • 한 페이지당 보여지는 사용자id 갯수를 지정한다.
  • 전체 사용자id 갯수를 페이지당 보여지는 사용자id 갯수로 나눈 값을 전체 페이지 수(totalPage)로 저장한다.
  • 화면에 한 번에 보여질 페이지 숫자의 갯수(PAGES_PER_LIST)를 지정한 후 해당 페이지들을 저장하는 배열을 만든다. PAGES_PER_LIST는 화살표 버튼이 눌릴 때마다 totalPage와 비교하여 보여져야 할 새로운 페이지 배열을 만든다.
  • 현재 보여져야 할 데이터는 현재 페이지 번호 - 1한 값과 한 페이지 당 보여져야 하는 id의 갯수를 곱한 값부터, 현재 페이지 번호와 한 페이지 당 보여져야 하는 id 갯수를 곱한 값까지 전체 id배열에서 slice해온 결과가 된다.

시행착오의 기록 🤕

하나,

테이블과 페이지네이션이 특정 상태들을 공유해야만 한다는 사실을 간과해버리고 Pagination.jsx에 상태와 기능들을 구현했다.
테이블에 페이지마다 다른 데이터를 보여주려고 하니 그제서야 상태가 알맞지 않은 곳에서 관리되고 있다는 걸 깨달았고, 테이블과 페이지네이션을 함께 가지고 있는 상위 컴포넌트인AccountManagement.jsx로 상태와 기능들을 마이그레이션 해야했다...😂

설계를 할 때는 어떤 컴포넌트들이 상태를 공유하고, 이 상태가 관리되어야 하는 최상위 컴포넌트는 어디인지를 구체적으로 계획을 세우자!

둘,

페이지가 선택될 때마다 테이블에 보여지는 데이터를 바꿔주기 위해서는 이전에 보여졌던 데이터의 시작점과 종료점을 찾아야 한다.
그 기준은 인덱스가 되므로, 이전 페이지의 시작 인덱스와 종료 인덱스를 상태로써 저장해두었다가 인덱스를 기준으로 다시 선택된 페이지의 데이터를 전체 데이터 배열에서 잘라오려고 했다.
(예를 들면 const [firstIndex, setFirstIndex] = useState(0); 이런식으로...)

하지만 한 페이지 당 보여져야 하는 데이터의 갯수만 지정이 된다면 현재 페이지번호를 곱해서 필요한 인덱스를 판단할 수가 있었다.

  const currentPageData = tableData.slice(
    (currentPage - 1) * ITEMS_PER_PAGE,
    currentPage * ITEMS_PER_PAGE,
  );

이렇게 구현할 경우 로직이 훨씬 간단해 진다는 걸 알 수 있었다!

셋,

현재 선택 가능하게 보여지는 5개의 페이지 중 마지막 페이지가 있는지 없는지를 판단하는 로직을 구현하기 위해서 매번 현재 보여지는 페이지 배열의 마지막 요소에 PAGES_PER_LIST를 더해주며 검사를 했었는데,
팀원분의 리팩토링 덕분에 useState()로 만들어지는 set함수가 이전상태를 parameter로 받아올 수 있단 걸 알게 되었다!

//초기값
  const [showingNum, setShowingNum] = useState({
    start: 1,
    end: PAGES_PER_LIST,
  });

  useEffect(() => {
    const lessThanFive = totalPage <= PAGES_PER_LIST;
    lessThanFive
      ? setShowingNum(prev => ({ ...prev, start: 1, end: totalPage }))
      : setShowingNum(prev => ({ ...prev, start: 1, end: PAGES_PER_LIST }));
  }, [totalPage]);

이전 상태와의 직접적인 비교가 가능해지면서 코드가 훨씬 간결하고 명확해졌다. :)

+추가
이전 상태 (prev)를 parameter로 넘겨줄 수 있기 때문에 set함수에 이전 상태를 인자로 받는 함수를 인자로써 전달해줄 수도 있다.

//왼쪽 화살표가 눌렸을 때
  const changePageNumbersBackward = () => {
    currentPage > PAGES_PER_LIST &&
      setShowingNum(prev => arrowHandler(prev, -1, totalPage));📌📌📌
  };

//오른쪽 화살표가 눌렸을 때
  const changePageNumberForward = () => {
    showingNum.end <= totalPage &&
      setShowingNum(prev => arrowHandler(prev, 1, totalPage));📌📌📌
  };

//util의 arrowHandler 함수
const arrowHandler = (prev, sign, totalPage) => {
  const PAGES_PER_LIST = 5;
  const nextIndex = prev.end + PAGES_PER_LIST;
  let res;
  if (sign === 1) {
    res = nextIndex > totalPage ? totalPage : nextIndex;
  } else if (sign === -1) {
    res =
      prev.end - prev.start < 4
        ? prev.start + 4 - PAGES_PER_LIST
        : prev.end - PAGES_PER_LIST;
  }
  return { ...prev, start: prev.start + PAGES_PER_LIST * sign, end: res };
};

export default arrowHandler;


결과물

UI

테이블에 들어가는 데이터를 10개 단위로 페이징 처리한다.

Code

페이지네이션이 필요한 페이지는 관리자(Admin)페이지에서 아이디를 테이블로 관리하는 부분.
테이블에 들어갈 데이터를 페이지 단위로 보여줘야 하기에 대략적인 구조는 이렇다:

*부모 컴포넌트 > 자식 컴포넌트
Admin.jsx > AccountManagement.jsx > Pagination.jsx > PageButton.jsx

AccountManagement.jsx

  • totalPage: 현재 데이터에서 10개씩 데이터를 쪼갰을 때 나올 수 있는 총 페이지의 수
  • currentPage: 현재 선택된 페이지의 숫자
const ITEMS_PER_PAGE = 10;

export default function AccountManagement() {
  const [totalPage, setTotalPage] = useState(1);
  const [currentPage, setCurrentPage] = useState(1);
  const [tableData, setTableData] = useState([]);

  useEffect(() => {
    setTableData(localStorageHelper.getItem('userInfo') || []);
  }, []);

  useEffect(() => {
    const lastPage = Math.ceil(tableData.length / ITEMS_PER_PAGE);
    setTotalPage(lastPage ? lastPage : 1);
  }, [tableData]);

  const handleOnSearch = useCallback(result => {
    setTableData(result);
  }, []);

  const currentPageData = tableData.slice(
    (currentPage - 1) * ITEMS_PER_PAGE,
    currentPage * ITEMS_PER_PAGE,
  );

  return (
    <TableContainer>
      //부분 생략...
      <Table
        dataProps={dataProps}
        currentPageData={currentPageData}
        tableData={tableData}
        setTableData={setTableData}
      />
      <Pagination
        totalPage={totalPage}
        currentPage={currentPage}
        setCurrentPage={setCurrentPage}
      />
    </TableContainer>
  );
}

Pagination.jsx

ex. < 6, 7, 8, 9, 10 >

  • showingNum: 사용자가 선택할 수 있게 한번에 화면에 보여지는 페이지들. 이 경우 한번에 5개 페이지에 대한 페이지 숫자들이 보여진다.
    위 예제에서 숫자들(6, 7, 8, 9, 10)에 해당.
  • isFirstPage: 현재 보여지는 5개의 페이지 옵션 중 가장 첫 페이지.
    위 예제에서는 6.
  • isLastPage: 현재 보여지는 5개의 페이지 옵션 중 가장 마지막 페이지.
    위 예제에서는 10.
import { arrowHandler, getEmptyArray } from './utils';

const PAGES_PER_LIST = 5;

export default function Pagination({ totalPage, currentPage, setCurrentPage }) {
  const [showingNum, setShowingNum] = useState({
    start: 1,
    end: PAGES_PER_LIST,
  });

  const changePageNumbersBackward = () => {
    currentPage > PAGES_PER_LIST &&
      setShowingNum(prev => arrowHandler(prev, -1, totalPage));
  };

  const changePageNumberForward = () => {
    showingNum.end <= totalPage &&
      setShowingNum(prev => arrowHandler(prev, 1, totalPage));
  };

  useEffect(() => {
    const lessThanFive = totalPage <= PAGES_PER_LIST;
    lessThanFive
      ? setShowingNum(prev => ({ ...prev, start: 1, end: totalPage }))
      : setShowingNum(prev => ({ ...prev, start: 1, end: PAGES_PER_LIST }));
  }, [totalPage]);

  useEffect(() => {
    setCurrentPage(showingNum.start);
  }, [showingNum, setCurrentPage]);

  const isFirstPage = showingNum.start === 1;
  const isLastPage = showingNum.end === totalPage;
  const pages = getEmptyArray(showingNum.start, showingNum.end);

  return (
    <PageListContainer>
      <ArrowButton
        type="back"
        inActive={isFirstPage}
        disabled={isFirstPage}
        changePageNumbersBackward={changePageNumbersBackward}
      />
      {pages.map((page, idx) => {
        return (
          <PageButton
            key={`pageNumber-${idx + 1}`}
            page={page}
            setCurrentPage={setCurrentPage}
            isActive={page === currentPage}
          />
        );
      })}
      <ArrowButton
        type="next"
        inActive={isLastPage}
        disabled={isLastPage}
        changePageNumberForward={changePageNumberForward}
      />
    </PageListContainer>
  );
}

PageButton.jsx

function PageButton({ page, setCurrentPage, isActive }) {
  const handleClickButton = () => {
    setCurrentPage(page);
  };

  return (
    <PageButtonContainer isActive={isActive}>
      <StyledButton onClick={handleClickButton} isActive={isActive}>
        {page}
      </StyledButton>
    </PageButtonContainer>
  );
}

Reference

https://chanhuiseok.github.io/posts/react-12/
https://chanhuiseok.github.io/posts/react-13/

profile
프린이의 코묻은 코드가 쌓이는 공간

0개의 댓글