@@@결과 미리보기@@@
1. 구현
1. 기본 로직
problemsData는 데이터 배열이다!
- 현재 페이지 state, 한 페이지에 몇 개의 아이템을 보여줄지, 전체 페이지 수
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const totalPages = Math.ceil(problemsData.length / itemsPerPage);
currentPage를 통해 현재 페이지를 useState로 선언해 주고, 한 페이지에 몇 개의 항목이 들어갈지 itemsPerPage를 통해 지정한다.
이후 전체 페이지 수를 구하기 위해서 (데이터/한 페이지에 몇 개의 항목이 들어갈지)를 올림 하여 계산한 totalPages를 선언한다. Math.ceil()을 통해 올림을 할 수 있다.
- 한 리스트에 표현되는 아이템 슬라이싱
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentItems = problemsData.slice(startIndex, endIndex);
예를 들어 내가 2page에 있다고 할 때, 2page에 해당하는 부분만큼 데이터에서 슬라이싱 해서 보여주는 것이라고 생각하면 된다.
2page에서 맨 위에 뜨는 행을 startIndex, 맨 아래에 뜨는 행을 endIndex로 생각하고,
현재 내가 보고 있는 페이지의 item 배열을 currentItems, 이는 원래 데이터에서 startIndex부터 endIndex까지 자른 결과이다.
- return 문
{currentItems.map((item) => (
<ListItem
key={item.number}
problemNumber={item.number}
problemTitle={item.title}
problemCorrect={item.correct}
problemSubmit={item.submit}
problemProportion={item.proportion}
></ListItem>
))}
이처럼 내가 보고 있는 현재 페이지 부분만 보이면 되기 때문에, currentItems를 map으로 보여준다.
2. 버튼 및 리스트 구현
- 이전버튼과 다음버튼
<PageButton
disabled={currentPage === 1}
onClick={() => setCurrentPage((prev) => prev - 1)}
>
이전
</PageButton>
<PageButton
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((prev) => prev + 1)}
>
다음
</PageButton>
이전 버튼과 다음버튼은 2가지 요구사항을 만족해야 한다.
1. 이전 or 다음 버튼을 누르면 현재 페이지가 전 페이지로 이동하거나 다음 페이지로 이동해야 한다.
2. 첫 번째 페이지에서는 이전이 눌리지 않고, 마지막 페이지에서는 다음이 눌리지 않아야 한다.
1번 조건을 만족하기 위해서 위 코드를 보면,
onClick={() => setCurrentPage((prev) => prev +- 1)}
이처럼 onClick에서 currentPage state를 이전보다 1을 더해주거나 빼주는 것을 확인할 수 있다.
2번 조건을 만족하기 위해서
disabled={currentPage === 1}
disabled={currentPage === totalPages}
disabled를 통해 버튼을 클릭하지 못하도록 막는 조건을 활용하여 구현하였다.
첫 페이지이거나, 마지막페이지인 경우 각 해당 버튼은 disabled 된다.
- 페이지 버튼
{[...Array(totalPages).keys()].map((page) => (
<PageButton
key={page}
active={currentPage === page + 1}
onClick={() => setCurrentPage(page + 1)}
>
{page + 1}
</PageButton>
))}
1. Array()
Array(5); // [undefined, undefined, undefined, undefined, undefined]
js에서는 Array(5)를 하면 길이 5에 해당하는 배열이 생긴다.
Array(totalPages)는 totalPage의 값이 현재 5이기 때문에, 길이가 5인 배열이 생긴다.
2. keys()
keys()는 배열의 길이만큼 인덱스 값을 반환하는 iterator를 생성한다.
Array(5).keys(); // Array Iterator { 0, 1, 2, 3, 4 }
keys()를 하지 않은 경우 Array(totalPages)에는 값이 존재하지 않기 때문에, 이처럼 Iterator 반환하게 해 준다.
3. 전개 연산자 ...
keys()를 사용함으로써 배열이 아니기 때문에, 배열과 같이 만들어 주기 위해 스프레드 연산자(전개 연산자)를 사용하여 이를 배열과 같이 만들어 준다.
[...Array(5).keys()]; // [0, 1, 2, 3, 4]
이와같은 과정을 통해 1부터 totalPage까지의 배열을 생성하여 map()을 통해 PageButton을 반복하여 생성할 수 있다.
2. 결과 및 전체 소스코드
1. 결과화면
2. 전체 소스코드
(data의 양이 너무 많아 적당히 잘랐습니다)
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
const ProblemsList = () => {
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const totalPages = Math.ceil(problemsData.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentItems = problemsData.slice(startIndex, endIndex);
return (
<BaseContainer>
<ListContainer>
<ListHeader>
<HeaderNonTitle>문제</HeaderNonTitle>
<HeaderTitle>문제 제목</HeaderTitle>
<HeaderNonTitle>맞힌사람</HeaderNonTitle>
<HeaderNonTitle>제출</HeaderNonTitle>
<HeaderNonTitle>정답 비율</HeaderNonTitle>
</ListHeader>
{currentItems.map((item) => (
<ListItem
key={item.number}
problemNumber={item.number}
problemTitle={item.title}
problemCorrect={item.correct}
problemSubmit={item.submit}
problemProportion={item.proportion}
></ListItem>
))}
</ListContainer>
<Pagination>
<PageButton
disabled={currentPage === 1}
onClick={() => setCurrentPage((prev) => prev - 1)}
>
이전
</PageButton>
{[...Array(totalPages).keys()].map((page) => (
<PageButton
key={page}
active={currentPage === page + 1}
onClick={() => setCurrentPage(page + 1)}
>
{page + 1}
</PageButton>
))}
<PageButton
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((prev) => prev + 1)}
>
다음
</PageButton>
</Pagination>
</BaseContainer>
);
};
export default ProblemsList;
const BaseContainer = styled.div`
width: 100%;
height: calc(100vh - 80px);
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
align-items: center;
`;
const ListContainer = styled.div`
width: 1000px;
border-radius: 15px;
`;
const ListHeader = styled.div`
background-color: #f2f2f2;
display: flex;
font-weight: bold;
height: 40px;
border-radius: 12px 12px 0 0;
align-items: center;
justify-content: center;
`;
const HeaderNonTitle = styled.div`
width: 150px;
text-align: center;
`;
const HeaderTitle = styled.div`
width: 600px;
text-align: center;
`;
const Pagination = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
gap: 10px;
`;
const PageButton = styled.button`
padding: 10px 15px;
background-color: ${({ active }) => (active ? '#6439ff' : '#f2f2f2')};
color: ${({ active }) => (active ? '#fff' : '#000')};
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
&:disabled {
background-color: #ccc;
cursor: not-allowed;
}
`;
const ListItem = ({
problemNumber,
problemTitle,
problemCorrect,
problemSubmit,
problemProportion,
}) => {
const nav = useNavigate();
return (
<ItemContainer>
<HeaderNonTitle>{problemNumber}</HeaderNonTitle>
<ItemTitle onClick={() => nav('/matching/battle')}>
{problemTitle}
</ItemTitle>
<HeaderNonTitle>{problemCorrect}</HeaderNonTitle>
<HeaderNonTitle>{problemSubmit}</HeaderNonTitle>
<HeaderNonTitle>{problemProportion}</HeaderNonTitle>
</ItemContainer>
);
};
const ItemContainer = styled.div`
border-bottom: 1.5px solid #f2f2f2;
display: flex;
height: 45px;
align-items: center;
justify-content: center;
`;
const ItemTitle = styled.a`
width: 600px;
text-align: center;
font-weight: 550;
&:hover {
color: #6439ff;
}
text-decoration: underline;
`;
const problemsData = [
{
number: 13460,
title: '구슬 탈출 2',
correct: 17444,
submit: 98706,
proportion: '28.319%',
},
{
number: 12100,
title: '2048 (Easy)',
correct: 17399,
submit: 98983,
proportion: '26.832%',
},
{
number: 3190,
title: '뱀',
correct: 23842,
submit: 82069,
proportion: '41.519%',
},
{
number: 13458,
title: '시험 감독',
correct: 23419,
submit: 100627,
proportion: '29.730%',
},
{
number: 14499,
title: '주사위 굴리기',
correct: 18565,
submit: 55008,
proportion: '45.389%',
},
{
number: 14500,
title: '테트로미노',
correct: 25542,
submit: 100719,
proportion: '36.575%',
},
{
number: 14501,
title: '퇴사',
correct: 36377,
submit: 107483,
proportion: '50.597%',
},
{
number: 14502,
title: '연구소',
correct: 35765,
submit: 109903,
proportion: '55.293%',
},
{
number: 14503,
title: '로봇 청소기',
correct: 26274,
submit: 70530,
proportion: '53.925%',
},
];
'React.js > React 프로젝트 및 구현' 카테고리의 다른 글
[React.js] 리액트 반응형 구현 (styled-component,media query, grid를 곁들인) (2) | 2024.11.12 |
---|---|
[React.js] 리액트 최적화를 위한 Intersection Observer 사용 (더보기 버튼/스크롤) (2) | 2024.11.02 |
[React.js] 리액트 글자 타이핑 효과 구현 (직접 구현/전체 코드) (0) | 2024.10.27 |
[React.js] 리액트 원페이지 스크롤 구현 (직접 구현/스크롤 유도) (4) | 2024.10.27 |
[React.js] 구글 페이지 번역 (전체 번역 및 언어 선택 버튼) (4) | 2024.10.18 |