일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- set
- async
- 슬라이딩윈도우
- Next.js
- 타입스크립트
- 배열중복요소제거
- interface
- Spring
- supabase 페이지네이션
- 페이지네이션
- react
- 스크롤이벤트
- generic
- array
- mainapplication.kt
- TS
- map
- 리액트네이티브아이콘
- meatadata
- extends
- 이진탐색
- 글또10기
- app.post
- app:compiledebugkotlin
- 상속
- javascript
- materialicons
- 안드로이드빌드에러
- Filter
- reactnative
- Today
- Total
rhanziy
Next.js - 페이지네이션 구현기(custom pagination, with supabase) 본문
페이지네이션 라이브러리를 안쓰고 큰 흐름을 파악하고자 직접 구현해봤다.
의도한대로 동작은 되었는데 3번은 갈아 엎은듯. 하하. 페이지네이션이 이렇게 복잡한 것일 줄이야.
그래서 구현과정을 정리해보고자 제목을 [페이지네이션 구현기]로 정했다.
1차 시도 데이터 배열 slice
처음에는 단순하게 데이터가 리스트가 담긴 배열을
현재페이지 * 보여줄 페이지 개수 ~ 다음페이지 * 보여줄 페이지 개수로 slice해서 보여줬었는데
데이터가 많아지면 비효율적이기때문에 일찌감치 리팩토링 했다.
arr.slice((currentPage - 1) * itemCountPerPage, Math.min(currentPage * itemCountPerPage, totalItems)
2차 시도 searchParams
useSearchParams를 통해 url에서 페이지 번호를 받아와서 바뀔때마다 data fetch해오는게 정석적인 방법인 것 같아서 구현해보았다.
가져올 데이터의 테이블과 총 list데이터 갯수를 params로 넘겨받는 usePagination 공통 훅을 하나 만들었다.
접근법은 맞는데, 중복되는 코드들과 데이터를 fetch해오는 비동기함수를 useEffect로 처리하는게 최적화된 방법이 아닌 것 같아서 다시 개선했다.
👇🏻 문제라고 생각했던 부분
useEffect(() => {
const fetch = async () => {
try {
const lists: T[] = await fetchPageData(
table,
totalItems,
currentPage,
itemCountPerPage,
);
setFetchData(lists);
} catch (error) {
console.error('Error fetching list', error);
}
};
fetch();
}, [table, currentPage, totalItems]);
최종코드
next app router에서는 server component의 props로 searchParams가 전달된다고 한다.
그래서 server component의 props로 전달받은 searchParams를 하위 컴포넌트의 props로 전달하여 url의 page값이 바뀌면 데이터를 fetch 해오도옥 처리했다.
/reviews/page.tsx
export default async function Reviews({
searchParams,
}: {
searchParams: { page: string };
}) {
// 1. /reviews 페이지에 접근하면 url의 page key의 값을 받아오고 있으면 number처리, 없으면 1페이지 처리
const page = searchParams.page ? parseInt(searchParams.page) : 1;
// 2. 해당 페이지의 review데이터와 총 item 갯수를 불러오는 비동기 함수
const { data, count } = await getReviews(page);
// 3. ITEMCOUNTPERPAGE는 1 페이지당 보여줄 아이템 갯수이고 총 페이지를 계산한다.
const pageCount = Math.ceil(count / ITEMCOUNTPERPAGE);
// 4. 사용자의 잘못된 접근 처리
if (page < 1) {
redirect('/reviews?page=1');
} else if (page > pageCount) {
redirect(`/reviews?page=${pageCount}`);
}
return (
<div className={wrapper}>
<Suspense fallback={<ReviewSkeleton />}>
<ReviewList reviews={data} page={page} pageCount={pageCount} />
</Suspense>
</div>
);
}
supabase에서 데이터를 불러오는 getReviews 비동기 함수
export async function getReviews(page: number = 1) {
const supabase = createClient();
try {
const { data, count } = await supabase
.from('reviews')
.select(
'id, created_at, age, gender, nickname, category, content, date',
// 테이블의 전체 item 갯수를 반환함!
{ count: 'exact' },
)
.order('created_at', { ascending: false })
// range를 통해 해당하는 페이지 범위의 데이터만 가져온다.
.range((page - 1) * ITEMCOUNTPERPAGE, page * ITEMCOUNTPERPAGE - 1);
// data가 undefined일 경우 pageCount 계산을 위한 처리
if (!data || data.length === 0) {
return { data: [], count: 0 };
}
return { data, count };
} catch (error) {
console.log(error);
throw error;
}
}
client side - ReviewList
여기서 유저의 페이지 변경 이벤트를 처리하는 usePagination은 다른 페이지에서도 사용하니 hook으로 만들어 놓았고,
customPagination 컴포넌트에서 onPageChange 핸들러로 사용된다.
'use client';
import ReviewComponent from './ReviewComponent';
import CustomPagination from '@/app/components/pagination/CustomPagination';
import usePagination from '@/app/components/pagination/usePagination';
import { IReview } from '@/app/types';
import * as styles from '../style/style.css';
export function ReviewList({
reviews,
page,
pageCount,
}: {
reviews: IReview[];
page: number;
pageCount: number;
}) {
const handlePageChange = usePagination(pageCount);
if (!reviews || reviews.length === 0) {
return (
<div className={styles.emptyReviewContainer}>
아직 작성된 후기가 없어요.
</div>
);
}
return (
<>
<div className={styles.reviewWrapper}>
<ReviewComponent reviewList={reviews} />
</div>
<CustomPagination
currentPage={page}
pageCount={pageCount}
onPageChange={handlePageChange}
/>
</>
);
}
client side - usePagination hook
여기서 params로 받아오는 keyword는 multiplePagination을 위한 키워드.
admin페이지에서는 한페이지에 2개의 데이터 리스트를 보여주며, 각각 petPage, userPage로 페이지 번호를 받아온다.
기본값은 page.
다음편에서 multiplePagination 구현한걸 작성하도록 하겠슴. 원래 컴포넌트를 분리했어야하지만 귀찮아서 한 파일에 구현해봤다...
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';
const usePagination = (keyword = 'page') => {
const searchParams = useSearchParams();
const router = useRouter();
const handlePageChange = useCallback(
(page: number) => {
const newParams = new URLSearchParams(searchParams.toString());
newParams.set(keyword, page.toString());
router.push(`?${newParams.toString()}`);
},
[searchParams, keyword, router],
);
return handlePageChange;
};
export default usePagination;
client side - CustomPagination
import * as styles from './style.css';
interface Props {
currentPage: number; // 현재 페이지
pageCount: number; // 전체 페이지 개수
onPageChange: (page: number) => void; // 페이지 변경 핸들러
showPageCount?: number;
}
export default function CustomPagination({
currentPage,
pageCount,
onPageChange,
showPageCount = 4,
}: Props) {
const noPrev = currentPage === 1;
const noNext = currentPage === pageCount;
const handlePrev = () => {
if (noPrev) return;
onPageChange(currentPage - 1);
};
const handleNext = () => {
if (noNext) return;
onPageChange(currentPage + 1);
};
// 페이지가 많아지면 가운데 생략처리를 위한 계산
const halfVisible = Math.floor(showPageCount / 2);
// 시작 페이지와 끝 페이지 계산
let startPage = Math.max(currentPage - halfVisible, 1);
let endPage = Math.min(startPage + showPageCount - 1, pageCount);
// 끝 페이지가 전체 페이지 수보다 적으면 시작 페이지 조정
if (endPage - startPage < showPageCount - 1) {
startPage = Math.max(endPage - showPageCount + 1, 1);
}
// 페이지 배열 생성
const pages = Array.from(
{ length: endPage - startPage + 1 },
(_, i) => startPage + i,
);
return (
<>
{pageCount > 0 && (
<div className={styles.paginationContainer}>
<button
className={styles.controlButton}
onClick={handlePrev}
disabled={noPrev}
>
이전
</button>
{/* 첫 번째 페이지 버튼과 생략 기호 */}
{startPage > 1 && (
<>
<button
className={styles.paginationButton}
onClick={() => onPageChange(1)}
>
1
</button>
{startPage > 2 && <span>...</span>} {/* 생략 기호 */}
</>
)}
{/* 페이지 버튼 */}
{pages.map((page) => (
<button
key={page}
className={`${styles.paginationButton} ${
page === currentPage ? styles.active : ''
}`}
onClick={() => onPageChange(page)}
>
{page}
</button>
))}
{/* 마지막 페이지 버튼과 생략 기호 */}
{endPage < pageCount && (
<>
{endPage < pageCount - 1 && <span>...</span>} {/* 생략 기호 */}
<button
className={styles.paginationButton}
onClick={() => onPageChange(pageCount)}
>
{pageCount}
</button>
</>
)}
<button
className={styles.controlButton}
onClick={handleNext}
disabled={noNext}
>
다음
</button>
</div>
)}
</>
);
}
구현영상
혹시 더 좋은 방법이있거나 개선점을 발견하시면 언제든 댓글 남겨주시면 감사하겠습니다^____^
'Next.js' 카테고리의 다른 글
Next.js - Open Graph를 통해 SEO 도움주기 (5) | 2024.10.29 |
---|---|
Next.js - SEO 적용하기 2탄(sitemap과 robots.txt) (2) | 2024.10.21 |
Next.js - SEO 적용하기 1탄(정적 metadata) (0) | 2024.10.14 |
Next.js14 - nodemailer 라이브러리를 통해 이메일 전송(vercel) (1) | 2024.07.09 |
Next.js - 모바일 페이지네이션 적용(react-paginate) (0) | 2024.04.22 |