-
[Next.js] 커서 기반 무한 스크롤 구현 (useSWRInfinite)개발공부/Next.js 2024. 5. 18. 21:32
본 글은 Next.js에서 useSWRInfinite를 사용해 커서 기반 무한 스크롤을 구현한 내용입니다.
목차
페이지네이션: 커서 기반 VS 페이지 기반
우선 페이지 기반과 커서 기반의 차이점 및 장, 단점에 대해 정리했다.
페이지 기반 (Page-based Pagination)
- 페이지 번호와 오프셋(offset)을 사용해 특정 페이지의 데이터를 가져오는 방식
- 장점
- 구현이 간단 (페이지 번호와 오프셋을 사용해)
- 특정 페이지 번호로 이동하기 때문에 직관적으로 사용 가능
- 많은 라이브러리와 프레임워크에서 기본적으로 지원하는 페이지네이션 방식
- 단점
- 데이터가 많을 수록 비효율적
- 데이터가 빈번하게 변경되는 경우, 데이터가 중복되거나 누락될 수 있음
커서 기반 (Cursor-based Pagination)
- 데이터의 특정 위치를 가리키는 커서를 사용해 다음 페이지의 데이터를 가져오는 방식
- 장점
- 큰 데이터에서 효율적, 페이지 기반과 달리 페이지 중간에 데이터 추가, 삭제 하더라도 영향 X
- 데이터 중복 또는 누락되는 경우가 적음
- 스크롤을 하여 데이터를 연속적으로 불러오는 경우 유리
- 단점
- 구현이 상대적으로 복잡
- 데이터가 특정 순서로 정렬되어 있어야 함
나는 중복되는 데이터를 제거하기 위해 커서 기반 페이지네이션을 선택했다.
이전에 진행했던 프로젝트들은 규모가 크지 않았고, 데이터도 별로 없었고, 추가 및 삭제가 많지 않아 "페이지 기반"으로 무한 스크롤을 구현했었다.
하지만 지금 진행하고 있는 프로젝트의 경우, 데이터도 많고, 추가 및 삭제가 빈번하게 일어날 것으로 추정되어 "커서 기반"을 선택하게 되었다.
무한 스크롤 구현 (useSWRInfinite)
우선 내 기술 스택을 정리하면 아래와 같다.
Next.js (app router), TypeScript, useSwr
1. 설치하기
yarn add swr
2. 사용하기
기본적인 구조는 아래와 같다.
import useSWRInfinite from 'swr/infinite'; const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite( getKey, fetcher?, options? )
(버전 별로 import 방식이 다르니 참고!)
먼저 파라미터에 대해 정리해봤다.
- getKey
- 페이지의 키를 반환하는 함수
- 인덱스와 이전 페이지 데이터를 받을 수 있음
- fetcher
- API 통신
- options
- useSWR이 지원하는 옵션
- initialSize: 초기에 로드해야 하는 페이지의 수
- revalidateAll (기본 값: false): 항상 모든 페이지의 갱신 시도
- revalidateFirstPage (기본 값: true): 항상 첫 번째 페이지 검증 시도
- persistSize (기본 값: false): 첫 페이지의 키가 변경될 때, 페이지 크기를 1로 초기화하지 않음 (initialSize가 설정된 경우 initialSize)
- parallel (기본 값: false): 여러 페이지를 병렬적으로 동시에 불러옴
반환 값은 아래와 같다.
- data: 각 페이지의 응답 값의 배열
- error: useSWR의 error와 동일
- isLoading: useSWR의 isLoading과 동일
- isValidating: useSWR의 isValidating과 동일
- mutate: useSWR의 바인딩 된 뮤테이트 함수와 동일하지만 데이터 배열을 다룸
- size: 가져올 페이지 및 반환될 페이지의 수
자세한 내용은 공식 문서를 참고하길 바란다.
위의 내용을 참고해 커서 기반 무한스크롤을 구현한 전체 코드는 아래와 같다.
'use client'; import useSWRInfinite from 'swr/infinite'; import { useEffect, useState } from 'react'; const Support = () => { const [lastCursorId, setLastCursorId] = useState<string>(""); // 마지막 커서 ID // 1. getKey 설정 // pageIndex: 현재 페이지 번호, previousPageData: 이전에 가져온 데이터 const getKey = (pageIndex: number, previousPageData: any) => { if (previousPageData && !previousPageData.hasNext) return null; // 끝에 도달한 경우 null 반환 // 첫 번쩨 요청에 보낼 url, 처음에 커서 정보를 모르기 때문에 구분 필요! if (pageIndex === 0) return ''; // 서버 url 입력 필요 setLastCursorId(previousPageData.lastCursorId); // 마지막 커서 ID 저장 // 첫 번째 요청 이후부터 마지막 요청전까지는 아래 url이 return될 예정, 반드시 커서 ID를 실어 보내야 함. if(Number(lastCursorId) <= Number(previousPageData.lastCursorId)) return ''; // 서버 url 입력 필요 else return null; } // 2. useSWRInfinite 사용해 데이터 요청 // fetchSupports는 axios를 사용 (axios.get(url)) const { data, setSize, mutate } = useSWRInfinite(getKey, fetchSupports, { revalidateOnFocus: true, shouldRetryOnError: false, // 에러 발생 시 재시도 비활성화 initialSize: 1 // 초기 로드 페이지 수 }); // 3. 불러온 데이터 관리 const [datas, setDatas] = useState<SupportListType[]>([]); // 불러온 데이터 state에 저장 useEffect(() => { // 불러온 데이터 저장 if(data) { let temps: SupportListType[] = [...datas]; // 현재 데이터 복사 data.forEach((d) => { d.data.forEach((item: SupportListType) => { if (!temps.some(existingItem => existingItem.questionId === item.questionId)) { temps.push(item); // 중복되지 않은 데이터만 추가 } }); }); setDatas(temps); } }, [data]); // 4. 스크롤 위치가 맨 아래에 도착 시 다음 데이터를 불러오기 위한 정보 셋팅 useEffect(() => { const handleScroll = () => { const { scrollTop, clientHeight, scrollHeight } = document.documentElement; if (scrollTop + clientHeight >= scrollHeight - 5) { setSize((prevSize) => prevSize + 1); // size 값이 변경되면 getKey 함수 내 로직 실행 } }; window.addEventListener('scroll', handleScroll); return () => { window.removeEventListener('scroll', handleScroll); }; }, [setSize]); return ( <section> {datas.map((d, index) => ( <Link href={``} key={index} className='h-[81px] p-[16px] border-b border-b-Neutral-L_Grey1 flex justify-between'> <SupportContent title={d.questionTitle} createdOn={d.createdOn} isAnswer={d.isAnswer}/> </Link> ))} </section>); };
마무리
커서 기반 페이지네이션 구현이 이번이 처음이라서 삽질을 많이 했었다..ㅎㅎ
다른 사람들은 내 코드를 참고해 불필요한 시간을 줄였으면 좋겠다! :)728x90'개발공부 > Next.js' 카테고리의 다른 글
[Next.js] useSWR + axios로 비동기로 데이터 가져오기 (0) 2024.05.26 [Next.js] Zod + React Hook Form 사용해 회원가입 페이지 만들기 (2) 2024.04.03 [Next.js, Node.js] 토큰 암호화 (crypto-js) (0) 2024.02.22 [Next.js] NextAuth 사용해 카카오톡 로그인 구현 (0) 2024.02.21 [Next.js] Hydration failed 에러 해결 (Unhandled Runtime Error) (0) 2024.02.17