난생 처음 8명이서 한 팀이 되어 과제를 진행했다.
'이렇게 많은 인원이 어떻게 누구 하나 소외되는 사람 없이 각자의 역할을 제대로 해낼 수 있게 분업을 할 수가 있을까?'
라는 고민이 있었지만, 페이지 별로, 혹은 기능별로 분업이 순조롭게 이어졌다.
스스로 로직을 고민하는 경험이 필요하다고 느껴오고 있었던만큼, 나는 이번에 어드민 페이지의 아이디들을 페이지별로 불어오는 페이지네이션(Pagination)을 맡겠다고 했다.
페이지네이션을 구현하려면 무엇을 상태로써 관리해야 할까?
대략적으로 구상해본 구현순서는 아래와 같다:
테이블과 페이지네이션이 특정 상태들을 공유해야만 한다는 사실을 간과해버리고 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;
테이블에 들어가는 데이터를 10개 단위로 페이징 처리한다.
페이지네이션이 필요한 페이지는 관리자(Admin)페이지에서 아이디를 테이블로 관리하는 부분.
테이블에 들어갈 데이터를 페이지 단위로 보여줘야 하기에 대략적인 구조는 이렇다:
*부모 컴포넌트 > 자식 컴포넌트
Admin.jsx
>AccountManagement.jsx
>Pagination.jsx
>PageButton.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>
);
}
ex. <
6, 7, 8, 9, 10 >
showingNum
: 사용자가 선택할 수 있게 한번에 화면에 보여지는 페이지들. 이 경우 한번에 5개 페이지에 대한 페이지 숫자들이 보여진다.isFirstPage
: 현재 보여지는 5개의 페이지 옵션 중 가장 첫 페이지.isLastPage
: 현재 보여지는 5개의 페이지 옵션 중 가장 마지막 페이지.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>
);
}
function PageButton({ page, setCurrentPage, isActive }) {
const handleClickButton = () => {
setCurrentPage(page);
};
return (
<PageButtonContainer isActive={isActive}>
<StyledButton onClick={handleClickButton} isActive={isActive}>
{page}
</StyledButton>
</PageButtonContainer>
);
}
https://chanhuiseok.github.io/posts/react-12/
https://chanhuiseok.github.io/posts/react-13/