[POB#09] Assignment 3: 자란다 기업 과제 기술 정리. 테이블, 테이블 페이지네이션

dongwon·2021년 8월 3일
3

원티드-프리온보딩

목록 보기
6/25
post-thumbnail

Assignment 3 과제를 통해 얻은 지식 정리 글입니다.
과제 레파지토리 : https://github.com/POB-Coconut/Assignment-3

🎯 Assignment 3 : 자란다

이번 기업 과제는 요구 사항이 많아 2번에 걸쳐 제출하는 방식입니다.
저는 이번에 관리자 로그인을 하면 등록한 계정 정보를 볼 수 있는 테이블 컴포넌트, 테이블 페이지 네이션, utils 함수 분리 및 jest를 이용한 test 작성을 맡았습니다. 사다리 타기로.. 이번 글은 테이블 컴포넌트, 테이블 페이지 네이션에 대한 글 입니다.

설계

유저 데이터 테이블이기에, 테이블이 길어질 수 있다 판단해 페이지 네이션을 이용해 구현하기로 설계했습니다. 또 페이지가 길어질 수 있어 페이지를 숨기는 기능을 구현해봤습니다.

📜 Table 컴포넌트

유저 정보를 list로 구현할 수도 있지만 요구 사항인 table 태그와 시멘틱 태그를 이용해 구현했습니다.
<thead> 태그에 요소들을 대표하는 행을, <tbody>에 정보들을 작성했습니다.

테이블 컴포넌트

const UserTable = () => {
  return (
    <>
      <TableContainer>
        <thead>
          <tr>
            <th>아이디</th>
            <th>이름</th>
            <th>나이</th>
            <th>주소</th>
            <th>카드 번호</th>
            <th>권한</th>
          </tr>
        </thead>
        <tbody>
          <UserData userData={currentUserData} />
        </tbody>
      </TableContainer>
      <Pagination
        userDataPerPage={DATA_PER_PAGE}
        totalUserData={userData.length}
        currentPage={currentPage}
        paginate={setCurrentPage}
      />
    </>
  );
};

UserData 컴포넌트

<tbody> 태그에 해당하는 내용입니다. <tbody> 태그엔 &&을 쓸 수 없어 삼항연산자로 표현했습니다.
리팩토링 과정을 거쳐 수정했습니다.

const UserData = ({ userData }) => {
  return (
    <>
      {userData.map((data) => (
            <tr key={data.id}>
              <td>{data.id}</td>
              <td>{data.name}</td>
              <td>{data.age}</td>
              <td>{data.address}</td>
              <td>{data.cardNumber}</td>
              <td>{data.userType}</td>
            </tr>
          ))
        : null}
    </>
  );
};

⚙️ 테이블 페이지네이션

페이지네이션을 하기 위해선 페이지네이션 컴포넌트 생성과 데이터를 자르는 작업을 거쳐야 합니다.

Table 컴포넌트

데이터를 자르는는 로직입니다.

const UserTable = () => {
  1️⃣ 상태관리
  const [userData, setUserData] = useState([]);
  const [currentUserData, setCurrentUserData] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [firstIndex, setFirstIndex] = useState(null);
  const [lastIndex, setLastIndex] = useState(null);

  // 과제 특성상 localStorage에 데이터를 저장해놨습니다.
  // localStorage에서 데이터 불러오는 로직
  useEffect(() => {
    setUserData(getStoreage(GET_USER_STORAGE_KEYWARD));
  }, []);

  2️⃣ 현재 페이지에 보여줄 데이터의 index 관련
  useEffect(() => {
    const nextLastIndex = currentPage * DATA_PER_PAGE;
    const nextFirstIndex = nextLastIndex - DATA_PER_PAGE;

    setLastIndex(nextLastIndex);
    setFirstIndex(nextFirstIndex);
  }, [currentPage, lastIndex]);

  3️⃣ 현재 페이지에 보여줄 데이터 set
  useEffect(() => {
    setCurrentUserData(currentUsers(userData, firstIndex, lastIndex));
  }, [userData, firstIndex, lastIndex]);

  if (userData.length === 0) {
    return <p>데이터가 비어 있습니다.</p>;
  }

  return (
    <>
      <TableContainer>
        <thead>
          <tr>
            <th>아이디</th>
            <th>이름</th>
            <th>나이</th>
            <th>주소</th>
            <th>카드 번호</th>
            <th>권한</th>
          </tr>
        </thead>
        <tbody>
          4️⃣ <UserData userData={currentUserData} />
        </tbody>
      </TableContainer>
      5️⃣ <Pagination
        totalUserData={userData.length}
        currentPage={currentPage}
        paginate={setCurrentPage}
      />
    </>
  );
};

코드 설명

1. 상태 관리 userData
userData : 가지고 있는 총 데이터.
currentUserData : 현재 페이지에 보여줄 데이터.
currentPage : 현재 페이지 숫자.
firstIndex : 현재 페이지에 보여줄 데이터의 첫 번째 index.
lastIndex : 현재 페이지에 보여줄 데이터의 마지막 index.

2. 현재 페이지에 보여줄 데이터의 index 관련
DATA_PER_PAGE : 한 페이지에 보여줄 데이터의 개수를 저장한 상수입니다.
nextLastIndex : 현재 페이지에 DATA_PER_PAGE를 곱한 값. 즉, 마지막 index
nextFirstIndex : 마지막 index에 DATA_PER_PAGE를 뺀 값. 즉, 첫 번째 index

3. 현재 페이지에 보여줄 데이터 set
currentUsers 함수: slice를 통해 나눈 data를 반환합니다.

export const currentUsers = (data, indexOfFirst, indexOfLast) => {
  const currentPageUsers = data.slice(indexOfFirst, indexOfLast);

  return currentPageUsers;
};

4. 나눈 데이터 display

5. Pagination을 위한 props

Pagination 컴포넌트 ( 페이지 리스트 자르기 )

Pagination 컴포넌트에선 Table 컴포넌트에서

const Pagination = ({ totalDataNum, paginate, currentPage }) => {
  1️⃣ 페이지 리스트 저장할 state
  const [pageLists, setPageLists] = useState([]);
  2️⃣ 총 페이지 숫자
  const totalPageNum = useMemo(
    () => getTotalPageNum(totalDataNum, DATA_PER_PAGE),
    [totalDataNum]
  );

  useEffect(() => {
    3️⃣ 페이지 리스트 생성
    const totalPageList = Array.from({ length: totalPageNum }, (v, i) => i + 1);

    4️⃣ 페이지 리스트 자르는 로직
    if (currentPage <= ONE_WAY_MIN_PAGE_NUM) {
      const displayedPageList = totalPageList.slice(0, ONE_WAY_MIN_PAGE_NUM + 2);
      setPageLists(displayedPageList);
    } else if (currentPage >= totalPageNum - 2) {
      const displayedPageList = totalPageList.slice(totalPageNum - 5, totalPageNum);
      setPageLists(displayedPageList);
    } else {
      const displayedPageList = totalPageList.slice(
        currentPage - ONE_WAY_MIN_PAGE_NUM,
        currentPage + ONE_WAY_MIN_PAGE_NUM - 1,
      );
      setPageLists(displayedPageList);
    }
  }, [totalPageNum, currentPage]);

  5️⃣ 끝 화살표 이동 함수
  const goEdgePage = useCallback(
    (edgePage) => {
      paginate(edgePage);
    },
    [paginate]
  );

  6️⃣ 이전, 다음 화살표 이동 함수
  const goNextToPage = useCallback(
    (nextToPage) => {
      if (nextToPage < 1 || nextToPage > totalPageNum) return;
      paginate(nextToPage);
    },
    [paginate, totalPageNum]
  );

  return (
    <PaginationContainer>
      <p onClick={() => goEdgePage(1)}>{"|<"}</p>
      <p onClick={() => goNextToPage(currentPage - 1)}>{"<"}</p>
      <PageUl>
        {pageLists.map((number) => (
          <PageLi key={number} active={currentPage === number}>
            <p onClick={() => paginate(number)}>{number}</p>
          </PageLi>
        ))}
      </PageUl>
      <p onClick={() => goNextToPage(currentPage + 1)}>{">"}</p>
      <p onClick={() => goEdgePage(totalPageNum)}>{">|"}</p>
    </PaginationContainer>
  );
};

코드 설명

1. 페이지 리스트 저장할 state
페이지 리스트들을 담을 변수입니다.
2. 총 페이지 숫자
페이지 리스트의 최댓값을 구합니다. useMemo를 이용해 재연산을 막았습니다.

const totalPageNum = useMemo(
  () => getTotalPageNum(totalDataNum, DATA_PER_PAGE),
  [totalDataNum]
);

getTotalPageNum 함수 : 총 데이터 수를 DATA_PER_PAGE로 나눈 고 오름 후 반환.

export const getTotalPageNum = (totalUserData, DATA_PER_PAGE) => {
  return Math.ceil(totalUserData / DATA_PER_PAGE);
};

3. 페이지 리스트 생성
totalPageList: totalPageNum의 수만큼의 배열 생성 후, 차례대로 index+1 씩 저장

// [1,2,3,4,5,6,7...]
const totalPageList = Array.from({ length: totalPageNum }, (v, i) => i + 1);

4. 페이지 리스트 자르는 로직
페이지 리스트가 너무 길 때를 대비해서 자르는 로직입니다.
ONE_WAY_MIN_PAGE_NUM한 방향으로 표현할 수 있는 페이지 리스트의 최소수 +1입니다.

ONE_WAY_MIN_PAGE_NUM가 3라면 이런 식으로...
|< < 1 2 3 4 5> >|
|< < 2 3 4 5 6 > >|

현재 페이지가 ONE_WAY_MIN_PAGE_NUM 보다 작다면, 페이지 리스트를 0~ONE_WAY_MIN_PAGE_NUM+2 까지 자릅니다.

if (currentPage <= ONE_WAY_MIN_PAGE_NUM) {
    const displayedPageList = totalPageList.slice(0, currentPage + ONE_WAY_MIN_PAGE_NUM +2);

이런 경우에 해당됩니다.
|< < 1️ 2 3 4 5 > >|

현재 페이지가 ONE_WAY_MIN_PAGE_NUM 보다 크다면, 페이지 리스트를 totalPageNum - 5, totalPageNum 까지 자릅니다.

이런 경우에 해당됩니다.
|< < 5 6 7 8 9 > >|

그 외 경우 가운데를 중심으로 양옆 2개씩 페이지 리스트를 표시합니다.

else if (currentPage > ONE_WAY_MIN_PAGE_NUM) {
      const displayedPageList = totalPageList.slice(
        currentPage - ONE_WAY_MIN_PAGE_NUM,
        currentPage + ONE_WAY_MIN_PAGE_NUM - 1,
      );

5. 끝 화살표 이동 함수
edgePage : 처음이거나 마지막 페이지의 숫자를 인자로 받아와 currentPage를 설정합니다.

6. 이전, 다음 화살표 이동 함수
nextToPage : 이전 혹은 이후의 페이지의 숫자를 인자로 받아와 양옆의 currentPage를 설정합니다.

🙏 테이블, 페이지 네이션 후기

페이지네이션이라고 하면 백엔드에서 구현하고 프론트엔드에서 page와 DATA_PER_PAGE를 이용해 호출하는 방식으로만 사용해봤습니다. 이번 기회에 프론트엔드에서 페이지 네이션 구현과 페이지 리스트를 자르는 로직을 구현하면서 페이지 네이션에 어느 정도 자신이 생긴 것 같습니다.

profile
데이원컴퍼니 프론트엔드 개발자입니다.

0개의 댓글