이번 심화코스에 신청하기 위해서 Pagination기능을 구현 해보려고 한다. 내 방법을 생각한 사람이 많이 없는것 같아 기록으로 남기려하며, 따로 라이브러리는 사용하지 않았다.
먼저 내가 구현할 페이지는 아래와 같이 프로토타입의 모습을 만들어두었다.
먼저 데이터가 어떻게 들어오는지 확인해 보기 위해서 MainPage에서 axios요청으로 데이터를 받아왔다.
MainPage.js
function MainPage () {
const [contentInfo, setContentInfo] = useState([]);
async function handlePostInfo(){
const result = await axios({
url : `${REACT_APP_API_URL}/posts`,
method: 'GET',
headers: {
"Content-Type": "application/json"
}
})
setContentInfo(result.data.reverse())
}
useEffect(() =>{
handlePostInfo()
},[])
}
useEffect로 페이지가 첫 구현이 될 때 정보를 받아와 setContentInfo에 넣어두어 constnetInfo를 저장해 두었다. reverse()로 구현한 이유는 숫자가 높을 수록 가장 최상단에 노출 될 것이라고 생각했기 때문이다.
데이터는 아래와 같이 들어오는 것을 확인할 수 있었다.
[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
},
{
"userId": 1,
"id": 3,
"title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
"body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
},
{
"userId": 1,
"id": 4,
"title": "eum et est occaecati",
"body": "ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit"
},
{
"userId": 1,
"id": 5,
"title": "nesciunt quas odio",
"body": "repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque"
},
...]
이후 이 데이터들을 컴포넌트를 나눠주어 구성해야 했다. styled-component를 사용해서 구성했기 때문에 아래와 같이 사용해주었다.
return (
<MainSection>
<SubjectWrap>
<Subject>게시판</Subject>
</SubjectWrap>
<BoardWrap>
<Board>
<ContentTitle>
<span>No.</span>
<span>제목</span>
<span>글쓴이</span>
</ContentTitle>
<Posts/>
<Pagenation/>
</Board>
</BoardWrap>
</MainSection>
)
Pagination을 구성하려면 한 페이지내에 몇개의 post가 있을지 정해야한다. 나의 경우에는 10개를 기준으로 했고 밑의 페이지는 5개 단위로 나누어져 있었으면 좋겠다 싶었다.
먼저 데이터를 눈에 보이도록 정렬하기 위해 posts라는 컴포넌트를 만들었다. 데이터를 MainPage.js에서 넘겨받아야 하기 때문에 props로 해당 데이터를 내려주었다.
MainPage.js
return (
<MainSection>
<SubjectWrap>
<Subject>게시판</Subject>
</SubjectWrap>
<BoardWrap>
<Board>
<ContentTitle>
<span>No.</span>
<span>제목</span>
<span>글쓴이</span>
</ContentTitle>
<Posts info={constentInfo}/>
<Pagenation/>
</Board>
</BoardWrap>
</MainSection>
)
Posts.js
function Posts ({info}){
return (
<ContentWrap>
{
info !== undefined ? info.map((data, idx)=> {
return (
<Content key={idx}>
<SpanWrap subject="no">{data.id}</SpanWrap>
<SpanWrap subject="title">{data.title}</SpanWrap>
<SpanWrap subject="no">{data.userId}</SpanWrap>
</Content>
)})
: (
<ImageWrap>
<img src={loadingImg} alt="loadingImg"/>
</ImageWrap>
)
}
</ContentWrap>
)
}
props로 받아오는 info는 axios로 비동기 요청을 통해 데이터를 받아온다. 첫 페이지 로딩시, 문제가 생길 수 있어, 초깃값을 빈 배열인 []로설정해주고 loading상태를 만들어 주었다.
이번 프로젝트를 하면서 어떻게하면 내가 원하는대로 구현이 될까 고민을 했던 부분이기도 하다. 위에서도 말했지만 10개로 표기하면 쉽게 pagination을 구성할 수 있었지만 Page표시가 많아지는게 싫어 5개만 두고 싶었고 넘어가면 6-10, 11-15, 16-20이와 같이 표시가 될 수 있도록 구현해 보고싶었다.
먼저, posts를 보여줄 갯수를 10개로 정했으니 데이터를 잘라야한다.
posts는 axios로 받아 contentInfo에 저장해 두었다. 이 데이터를 10개씩 잘라 posts.js에 props로 전달해주어 깔끔하게 보일 수 있도록 할 계획이다.
배열 데이터를 자르기 위해서는 시작점과 끝나는점을 지정하여 slice를 사용해보았다.
MainPage.js
const [page, setPage] = useState(1); //페이지
const limit = 10; // posts가 보일 최대한의 갯수
const offset = (page-1)*limit; // 시작점과 끝점을 구하는 offset
const postsData = (posts) => {
if(posts){
let result = posts.slice(offset, offset + limit);
return result;
}
}
위와 같이 MainPage에 별도의 함수를 구현하여 두었다. useState를 사용하여 page의 유동적인 값에 대해 초기값을 1로 설정해두었고, posts가 보이게 할 최대한의 갯수를 10개로 설정해두었다.
page 1번에는 0번부터 9번까지
page 2번에는 10번부터 19번까지
page 3번에는 20번부터 29번까지
...
반복이 되는걸 알 수 있다. slice는 두번째에 넣는 인덱스의 값은 포함되지 않기 때문에 위의 offset처럼 간단하게 만들 수 있었다.
다시 이 값을 props로 Posts에 넘겨주면 되겠다.
MainPage.js
return (
<MainSection>
<SubjectWrap>
<Subject>게시판</Subject>
</SubjectWrap>
<BoardWrap>
<Board>
<ContentTitle>
<span>No.</span>
<span>제목</span>
<span>글쓴이</span>
</ContentTitle>
<Posts info ={postsData(contentInfo)}/>
<Pagenation/>
</Board>
</BoardWrap>
</MainSection>
)
이렇게 설정해주면 posts에 들어가는 데이터는 10개씩 잘려 들어가게 된다.
pagination을 만들기 위해서는 어떤 변수들이 필요할까?
먼저 페이지의 전체수를 알기 위해서 데이터 전체에 대한 갯수가 필요할 것이고, posts에 나타낼 수 있는 최대 posts갯수인 limit도 알아야할 것이다.
현재 나타내고 있는 페이지와 변경될 페이지를 알아야할 것이다.
위와 같이 정리해보면 4가지가 나오게 된다.
1. page : 현재의 page
2. setPage : 변경될 page를 만드는 useState함수
3. limit : 한번에 posts의 최대 갯수
4. totalPosts : 데이터의 총 posts 갯수
위의 데이터들을 모두 Pagination의 props로 내려주자.
MainPage.js
return (
<MainSection>
<SubjectWrap>
<Subject>게시판</Subject>
</SubjectWrap>
<BoardWrap>
<Board>
<ContentTitle>
<span>No.</span>
<span>제목</span>
<span>글쓴이</span>
</ContentTitle>
<Posts info ={postsData(contentInfo)}/>
<Pagenation limit={limit} page={page} totalPosts={contentInfo.length} setPage={setPage}/>
</Board>
</BoardWrap>
</MainSection>
)
Mainpage에서 받은 props를 받고 Pagination을 어떻게 구상할지 생각해봐야한다.
먼저 간단한 Pagination을 만들어보자.
totalPosts와 limit을 이용해서 몇개의 Page가 만들어지는지 먼저 계산해 보아야한다.
이후 Page갯수에 맞는 배열을 만들고 mapping하여 버튼을 만들면 되겠다.
function Pagination ({totalPosts, limit, page, setPage}){
const numPages = Math.ceil(totalPosts/limit)
return (
<PageSection>
<ButtonWrap>
{Array(numPages).map((_, i) => {
return (
<Button
key={i+1}
onClick={() => setPage(i+1)}>
{i+1}
<Button/>
)})
}
<ButtonWrap/>
<PageSection/>
)
}
위와 같은 코드를 구성하면 1~10번까지의 버튼이 만들어지게 된다. 하지만 나는 10개의 Page가 있더라도 5개까지만 보여지고 next를 누르면 6-10번가지의 Page를 갖는 Pagination이 필요했다. 하여 이 부분을 조금 변경해보기로 했다.
먼저 CSS효과를 조금 정리하도록 첫번째 1번 버튼을 따로, 나머지 4개 2~5번까지의 버튼을 따로 묶어 만들계획을 세웠다.
첫 번째 할일은 바로 Pagination의 맨앞에 들어가는 버튼이다. 5개씩 잘라서 만들게 되면,
첫 번째 Page 1번
두 번째 Page 6번
세 번째 Page 11번
..
위와 같이 증가 될 것이다. 규칙성을 찾았으니 변수로 만들어 표현해야 한다. 내 경우에는
첫 번째 페이지 = 현재 페이지 - (현재페이지 % 5) + 1
마지막 페이지 = 현재 페이지 - (현재페이지 % 5) + 5
위와 같이 설정해주면 좋을것 같았다.
Pagination.js
function Pagination ({page, totalPosts, limit, setPage}){
let firstNum = page - (page % 5) + 1
let lastNum = page - (page % 5) + 5
return (
<PageSection>
<ButtonWrap>
<Button
onClick={() => {setPage(page-1)}
disabled={page===1}>
<
</Button>
<Button
onClick={() => setPage(firstNum)}
aria-current={page === firstNum ? "page" : null}>
{firstNum}
</Button>
{Array(4).fill().map((_, i) =>{
if(i <=2){
return (
<Button
border="true"
key={i+1+firstNum}
onClick={() => {setPage(firstNum+1+i)}}
aria-current={page === firstNum+1+i ? "page" : null}>
{firstNum+1+i}
</Button>
)
}
else if(i>=3){
return (
<Button
border="true"
key ={i+1}
onClick={() => setPage(lastNum)}
aria-current={page === lastNum ? "page" : null}>
{lastNum}
</Button>
)
}
})}
<Button
onClick={() => {setPage(page+1)}
disabled={page===numPages}>
>
</Button>
</ButtonWrap>
</PageSection>
)
}
위와 같이 설정해주니, 1-5까지의 Page를 구성할 수 있었다. 다면 여기서 치명적인 문제가 있었다.
5번을 누르면 onClick함수로 인해 setPage함수가 6으로 되어 버리면서 6-10까지로 확 바뀌어 버리는게 아닌가..5번을 클릭했어도 1-5번까지의 Page는 변하지 말아야했다.
이부분을 해결하기 위해 여러 방면으로 방법을 찾아보았지만, 해외에서도 직접 pagination을 구현하지 않고 대부분 라이브러리를 사용하는 바람에 큰 도움을 얻지는 못했다.
어떻게하면 좋을지 고민하다가 생각보다 쉽게 해결할 수 있었다. 1-5번까지 다 채워지고 나면 Next 화살표를 눌러 상태가 증가하게 한다는 점에서 착안하게 되었다.
현재는 page라는 state하나로 page와 posts를 모두 사용하고 있었다. 이러한 부분에서 나는 오류이기 때문에 page와 posts의 변수를 분리해서 사용하고 싶었다.
state를 하나 더 만들고 초깃값을 page로 설정해준다. 즉
const [currPage, setCurrPage] = useState(page)
위와 같이 설정해두고, firstNum
과 lastNum
의 page를 currPage로 바꿔주었다.
const [currPage, setCurrPage] = useState(page)
let firstNum = currPage - (currPage % 5) + 1
let lastNum = currPage - (currPage % 5) + 5
위와 같이 처리해두면 firstNum
의 값과 lastNum
은 page가 6번, 11번, 16번...등등 넘어갈때까지 고정적인 값을 갖게 된다.
즉,
page : 1, firstNum : 1, lastNum : 5 currPage : 1
page : 2, firstNum : 1, lastNum : 5 currPage : 1
page : 3, firstNum : 1, lastNum : 5 currPage : 1
page : 4, firstNum : 1, lastNum : 5 currPage : 1
page : 5, firstNum : 1, lastNum : 5 currPage : 1
page : 6, firstNum : 6, lastNum : 10 currPage : 6
page : 7, firstNum : 6, lastNum : 10 currPage : 6
page : 8, firstNum : 6, lastNum : 10 currPage : 6
page : 9, firstNum : 6, lastNum : 10 currPage : 6
page : 10, firstNum : 11, lastNum : 15 currPage : 6
page : 11, firstNum : 11, lastNum : 15 currPage : 11
page : 12, firstNum : 11, lastNum : 15 currPage : 11
page : 13, firstNum : 11, lastNum : 15 currPage : 11
여기서 주목할 부분은 bold체로 되어 있는 부분이다. page와 currPage를 분리시켜 두어 Next 화살표를 누를때 setCurrPage가 작동되게 만들어 Page가 6일때 값을 받도록 한다.
이렇게 되면 위의 오류를 해결할 수 있다.
function Pagination ({page, totalPosts, limit, setPage}){
const numPages = Math.ceil(totalPosts/limit)
const [currPage, setCurrPage] = useState(page)
let firstNum = currPage - (currPage % 5) + 1
let lastNum = currPage - (currPage % 5) + 5
//console.log({"currPage is":currPage, "firsNum is" : firstNum, "page is" : page})
return (
<PageSection>
<ButtonWrap>
<Button
onClick={() => {setPage(page-1); setCurrPage(page-2);}}
disabled={page===1}>
<
</Button>
<Button
onClick={() => setPage(firstNum)}
aria-current={page === firstNum ? "page" : null}>
{firstNum}
</Button>
{Array(4).fill().map((_, i) =>{
if(i <=2){
return (
<Button
border="true"
key={i+1}
onClick={() => {setPage(firstNum+1+i)}}
aria-current={page === firstNum+1+i ? "page" : null}>
{firstNum+1+i}
</Button>
)
}
else if(i>=3){
return (
<Button
border="true"
key ={i+1}
onClick={() => setPage(lastNum)}
aria-current={page === lastNum ? "page" : null}>
{lastNum}
</Button>
)
}
})}
<Button
onClick={() => {setPage(page+1); setCurrPage(page);}}
disabled={page===numPages}>
>
</Button>
</ButtonWrap>
</PageSection>
)
}
사실 이렇게 하는게 맞는가 싶다..구현은 했지만 뭔가 찜찜한 느낌이었다..요리조리 참고해서 원했던대로 계획한대로 구현할 수 있어서 다행이었다.
Daleseo reference
chanBLOG reference
Input type text reference