시작하기 전에...
내용이 많이 길다.
UI, function을 만들고
부모에서 데이터도 받아야 한고,
데이터 컨트롤러도 수정했다.
기존 진행된 내용
카드 리스트 이전 참고 내용
[카드 리스트 기본틀]
[카드 리스트 grid]
[한자 카드]
[블로그 카드]
[블로그 카드 이미지]
[페이지별 다른 카드 리스트]
[카드 수량 제한/컨트롤러]
/cantonese
한자 페이지/cantonese/word
단어 페이지/blog
블로그 페이지
에서 CardList
를 사용했다.
각 페이지에서 타입과 데이터를 전달해서CardList
에서 grid 디자인을 조절하고,CnCard
와 BlogCard
를 선택했다.
Pagination 계획
Pagination
Pagination은 이름처럼
페이지 번호 navigation이다.
페이지 번호를 클릭해 해당 페이지로 이동하거나
앞/뒤 버튼으로 이전/다음 페이지로 이동한다.
계획
따로 컴포넌트를 만들 생각이다.
사용되는 곳은 CardList
이지만...
만들면서 느낀 것은
각 페이지에서 데이터를 받기 때문에
각 페이지에서 사용하는 것이 적합했다.
- 다음/이전 버튼
- 현재 페이지 강조
- 현재 페이지 앞뒤 페이지 표시
- 앞 페이지와 첫 페이지 사이 생략표시
- 뒤 페이지와 마지막 페이지 사이 생략표시
- 파라미터 커리로 페이지를 사용
UI을 예시로 그려보면
◀ 1 … 4 5 6 … 10 ▶
Pagination 준비
컴포넌트 생성
[컴포넌트 편]참고
전에 만들었던 컴포넌트 폴더에 Pagination.js
을 추가했다.
// .\app\components\Pagination.js
const Pagination = () => {
return <>It's Pagination</>;
};
필요한 데이터
페이지네이션에서 필요한 변수가 많다.
- 현재 페이지
- 총 페이지 수량
- 현재 태그 ( SubNav 내용 [서브nav 편]참고 )
컴포넌트 자체에서 데이터를 확인할 수 있지만,
함수를 사용하고 변수 더 선언하는 것이
효과적이지 않을 것 같았다. (개인 생각)
react.js의 useSearchParams을 사용하면,
client로 만들어야 해서 좀 찜찜하기도 했다.
결론은..
부모로 부터 데이터를 받았다.
// .\app\components\Pagination.js
const Pagination = (props) => {
const currentPage = props.page;
const pageQty = props.maxPage;
const tag = props.tag;
return (
<>
It's Pagination {currentPage} / {pageQty} / {tag}
</>
);
};
아직 부모에서 데이터를 전달을 하지 않아서 오류가 발생한다.
getCardData 수정
현재 페이지와 카테고리(태그)도 이미 있는 데이터이다.
문제는 페이지 수량이다.getCardData
는 전체가 아닌maxCardInPage
의 수량 만큼만
데이터를 받아온다.
getCardData.js
에서 데이터를 제어하기 때문에getCardData
의 return 값을
객체로 변경하고allLength
로 전체 길이sortedData
으로 데이터를 반환했다.
// .\app\controller\getCardData.js - getCardData
export function getCardData(category, pageNum, maxCardInPage) {
const data = getCategoryData(category);
const dataLength = data.length;
const isFullCard = dataLength >= pageNum * maxCardInPage;
const cardLength = isFullCard ? maxCardInPage : dataLength % maxCardInPage;
const startCardNum = (pageNum - 1) * maxCardInPage;
const endCardNum = startCardNum + cardLength;
return {
allLength: dataLength,
sortedData: data.slice(startCardNum, endCardNum),
};
}
컴포넌트 적용
출력된 내용을 확인하기 위해
컴포넌트를 Blog페이지에 사용하고,
변경된 getCardData
도 적용시켰다.
/cantonese/page.js
한자 페이지/cantonese/word/page.js
단어 페이지/blog/page.js
블로그 페이지에서 사용된다. 한자와 단어는 우선 보류
---- 참고 -->
// .\app\blog\page.js - 추가 내용 외 생략
...
import Pagination from "@components/Pagination";
export default function Blog(props) {
const tag = props.searchParams.tag; // SubNav 편
const page = props.searchParams.page ?? 1; // controller 편
const maxCardInPage = 6; // controller 편
const data = getCardData(tag, page, maxCardInPage);
const blogData = data.sortedData;
const allLength = data.allLength;
const maxPage = Math.ceil(allLength / maxCardInPage);
return (
<>
...
<Pagination page={page} tag={tag} maxPage={maxPage}/>
</>
)
}
전체 길이 data.allLength
을 받아서
최대 수량 maxCardInPage
으로 나누고Math.ceil
을 사용해 올림처리를 했다.
Pagination 기능
만들기 전 계획을 보면,
- 7페이지 이하는 모든 번호가 노출된다.
- 첫 페이지가 아니면 prev 버튼이 있다.
- 끝 페이지가 아니면 next 버튼이 있다.
- 현재 페이지는 강조되고 click event가 없다.
그래서 생각한 방법은
페이지 번호 리스트를 만들고
리스트를 출력하는 방식이다.
setPageNums 페이지 배열
우선 함수 setPageNums를 만들었다.
// .\app\components\Pagination.js - setPageNums 필요 외 생략
const Pagination = (props) => {
const tag = props.tag;
const page = props.page;
const pageQty = props.maxPage;
const pageNums = setPageNums(page, pageQty);
function setPageNums(currentPage, lastPage){
if (lastPage < 7) {
let res = [];
for (let i = 1, i <= lastPage ; i++) {
res.push(i);
}
return res;
} else {
if (currentPage == 1) return [1, 2, -1, lastPage];
if (currentPage == 2) return [1, 2, 3, -1, lastPage];
if (currentPage == 3) return [1, 2, 3, 4, -1, lastPage];
if (currentPage == lastPage) return [1, -1, lastPage - 1, lastPage];
if (currentPage == lastPage - 1) return [1, -1, lastPage - 2, lastPage - 1, lastPage];
if (currentPage == lastPage - 2) return [1, -1, lastPage - 3, lastPage - 2, lastPage - 1, lastPage];
return [1, -1, currentPage - 1, currentPage, currentPage + 1, -2, lastPage];
}
}
...
};
오류 발생. 밑에서 설명
함수를 UI에 적용할 때 오류가 발생한다.
1 타입 오류
2 요소 중복 렌더링 오류
setPageNums
를 위에서부터 보면
- 현재 페이지와 마지막 페이지를 받는다.
- 7 페이지 이하이면
for
반복으로2
부터 시작하는 배열을 return
페이지 길이가 10으로 가정하면
- 현 페이지가 1 : 1 2 … 10
- 현 페이지가 2 : 1 2 3 … 10
- 현 페이지가 3 : 1 2 3 4 … 10
- 현 페이지가 마지막 페이지 : 1 … 9 10
- 현 페이지가 마지막 전 페이지 : 1 … 8 9 10
- 현 페이지가 마지막 전전 페이지 : 1 … 7 8 9 10
- 다른 경우 (5) : 1 … 4 5 6 … 10
--- 참고 ---
7개 요소 이상인 이유 : ◀ 1 … 4 5 6 … 10 ▶
1의 UI : 1 2 … 10 ▶
2의 UI : ◀ 1 2 3 … 10 ▶
3의 UI : ◀ 1 2 3 4 … 10 ▶
끝의 UI : ◀ 1 … 9 10
끝-1의 UI : ◀ 1 … 8 9 10 ▶
끝-2의 UI : ◀ 1 … 7 8 9 10 ▶
--- 참고 ---
setParams 링크 생성
- 번호를 클릭하면 해당 page 이동
- 앞/뒤 버튼 클릭하면 앞/뒤 페이지 이동
이상하게 SSL을 고집했다...
우선 Link 컴포넌트를 사용하고href
으로 객체를 전달할 때,
key값을 query로 객체를 전달하면 query 파라미터를 사용할 수 있다.
key값 pathname을 전달하지 않으면 같은 페이지의 query만 변경된다.
Next.js 공식문
예시: <Link href={query: {page=1}}> 1 <Link/>
tag
도 사용해야 한다.tag
가 없으면 pagination을 사용하면 전체 tag 페이지로 이동한다.
그래서 setParams
함수는
// .\app\components\Pagination.js - setParams 필요 외 생략
const Pagination = (props) => {
const tag = props.tag;
const pageQty = props.maxPage;
...
function setParams(num){
if (num < 1 || num > pageQty) return;
if (tag == "" || tag == null) {
return { page : num };
} else {
return { tag : tag, page: num };
}
}
}
- 클릭된
num
을 받는다 - 클릭된
num
이 1보다 작거나 끝보다 크면 무반응 tag
가 없으면 페이지 객체 returntag
이 있으면 태그 및 페이지 객체 return
Pagination UI
// .\app\components\Pagination.js - return 외 생략
import Link from "next/link";
...
const pageNums = setPageNums(page, pageQty);
...
return (
<div className="flex justify-center items-center">
{page != 1 ? <Link href={{query: setParams(page - 1)}}>◀<Link/> : null}
{pageNums.map((num) => {
return num == -1 || num == -2 ? (
<span key={("btn", num)}>...</span>
) : page == num ? (
<span key={("btn", num)}>{num}</span>
) : (
<Link key={("btn", num)} href={{ query: setParams(num) }}>
{num}
</Link>
);
})}
{pageQty == 0 || pageQty == 1 || pageQty == page ? null
: <Link href={{query: setParams(page + 1)}}>▶<Link/>}
</div>
)
가로 배치 중간 정렬
ul으로 만들려고 했지만
자녀가 Link 이어서 div로 만들었다.
tailwindcss로
flex를 사용해서 자녀를 가로로 배치하고 중간정렬을 했다.
<div className="flex justify-center items-center"></div>
Prev 버튼
{page != 1 ? <Link href={{query: setParams(page - 1)}}>◀<Link/> : null}
- 현재 페이지가
1
이 아니면◀
버튼 생성 setParams
으로 현재page
-1 의 링크 생성
페이지 버튼
{
pageNums.map((num) => {
return num == -1 || num == -2 ? (
<span key={("btn", num)}>...</span>
) : page == num ? (
<span key={("btn", num)}>{page}</span>
) : (
<Link key={("btn", num)} href={{ query: setParams(num) }}>
{num}
</Link>
);
});
}
pageNums
을map()
반복-1
,-2
는<span>...</span>
출력- 현재
page
와 같은 숫자는<span>
으로 출력 - 다른 숫자는
<Link>
출력.setParams
으로 경로 만듦
현재 페이지는 <span>
을 출력했다.
위의 예시는 아직 디자인이 없지만
실제로는 <span>
의 클라스로 text-green-500으로 강조했다.
Next 버튼
{pageQty == 0 || pageQty == 1 || pageQty == page ? null
: <Link href={{query: setParams(page + 1)}}>▶<Link/>}
- 총 페이지 수 0, 1 혹 현재
page
가 마지막 페이지는 null - 아니면
▶
버튼 생성. 링크setParams(page + 1)
에러 발생
오류가 발생했다.
- 번호가 이상하게 나온다.
- 타입 오류
- 생략 기호 중복
타입 오류
1번과 2번은 같은 이유로 발생하는 문제이다.
const page = props.page;
const pageQty = props.maxPage;
처음에 props로 현재 페이지와 마지막 페이지를 받았다.
하지만 props로 받은 내용은 string으로 되어 있었다.
형변환을 해서 간단하게 해결했다.
const page = Number(props.page);
const pageQty = Number(props.maxPage);
요소 중복 오류
setPageNums
에서
[1, -1, page-1, page, page+1, -1, lastPage] 반환할 때,
생략 …
중복되는 현상이 있었다.
React를 사용하면 요소가 중복 렌더링되는 경우를 많이 보게 된다.
React의 기능으로만 보면
렌더링이되는 경우를 고려해서 useEffect를 적절히 사용해야 했다.
Next.js에서는 React와 좀 다르게
요소가 중복으로 만들어지는 경우는
대부분 key값이 중복된 경우이다.
<span key={("btn", num)}>...</span>
key값을 반복 요소의 num을 사용했다.
앞뒤의 생략이 같은 -1
이어서 key도 같았다.
그래서
앞의 생략은 -1
을 그대로 사용하고
뒤의 생략은 -2
로 변경하고
조건문도 -2를 추가해서 문제를 해결했다.
다른 방법으로는
배열에서 -1
을 삭제하고
UI 부분에서
현재 페이지 전 번호,
현재 페이지 후 번호 출력에서
span을 포함해도 된다.
<>
<span>...</span>
<Link>{num}</Link>
</>
<>
<Link>{num}</Link>
<span>...</span>
</>
위의 코드를 응용해서
더 많은 방식을 생각할 수 있을 것이다.
결론
위의 방식으로...
드디어 pagination
을 완료했다.
// .\app\components\Pagination.js
const Pagination = (props) => {
const tag = props.tag;
const pageQty = Number(props.maxPages);
const page = Number(props.page);
const pageNums = setPageNums(page, pageQty);
function setPageNums(currentPage, lastPage){
if (lastPage < 7) {
let res = [];
for (let i = 1, i <= lastPage ; i++) {
res.push(i);
}
return res;
} else {
const prevPage = currentPage - 1;
const nextPage = currentPage + 1;
if (currentPage == 1) return [1, 2, -1, lastPage];
if (currentPage == 2) return [1, 2, 3, -1, lastPage];
if (currentPage == 3) return [1, 2, 3, 4, -1, lastPage];
if (currentPage == lastPage) return [1, -1, prevPage, lastPage];
if (currentPage == lastPage - 1) return [1, -1, prevPage, currentPage lastPage];
if (currentPage == lastPage - 2) return [1, -1, prevPage, currentPage, nextPage, lastPage];
return [1, -1, prevPage, currentPage, nextPage, -2, lastPage];
}
}
function setParams(num){
if (num < 1 || num > pageQty) return;
if (tag == "" || tag == null) {
return { page : num };
} else {
return { tag : tag, page: num };
}
}
return (
<div className="flex justify-center items-center">
{page != 1 ? <Link href={{query: setParams(page - 1)}}>◀<Link/> : null}
{pageNums.map((num) => {
return num == -1 || num == -2 ? (
<span key={("btn", num)}>...</span>
) : page == num ? (
<span key={("btn", num)}>{num}</span>
) : (
<Link key={("btn", num)} href={{ query: setParams(num) }}>
{num}
</Link>
);
})}
{pageQty == 0 || pageQty == 1 || pageQty == page ? null
: <Link href={{query: setParams(page + 1)}}>▶<Link/>}
</div>
)
}
한 페이지에 만들어서 코드가 길다.setPageNums
를 별도 파일로 만들면 조금 보기가 좋을 것 같다.
결론은...
Pagination을 만들기 위해
페이지 수량pageQty
이 필요했다.
총 수량은 끝 페이지 번호이기도 하다.
현재 페이지page
을 사용해서
Pagination의 형태를 결정했다.
[1 … 3 4 5 … 7 ] 은 기본이지만
[1 2 3 4 5 6] ,
[1 2 … 7]
같은 상황을 고려해서 배열을 만드는 setPageNums
함수를 만들었다.
UI는 배열을 반복해서 만들었다.
css로 현재 페이지를 강조하고
조건에 따라 span
혹 Link
을 사용했다.
Link
의 href는 setParams
을 사용해 만들었다.
무효 번호는 무반응.
유효 번호는 query에 들어갈 객체를 return한다.
Next 버튼이 없는 경우는 3가지이다.
1- 총 페이지 없음
2- 총 페이지 1개
3- 현재가 마지막 페이지
이렇게 Pagination을 완성했다.