front-end/Nextjs

[Next JS] React-Query로 UseInfinityQuery이용하여 무한 스크롤 구현하기 with express.js

hojung 2022. 8. 11.
728x90
반응형

1. 무엇을 하려하는가?

- 프론트엔드 개발을 진행하면서 좀 더 사용자 친화적인 ux에 대해 고민하게 되었다. 프론트엔드는 백엔드에서 가공된 데이터들을 어떻게 사용자들이 받아들이기 쉽게 보여주는가?에 대한 고민이 필수라고 생각한다. 그러던 와중 모든 콘텐츠 항목들을 보여주는 페이지와 같은 경우 옛날에는 페이지네이션을 통해 각 페이지마다 다른 데이터들을 보여주고 페이지를 넘길 때 화면이 깜빡거리면서 새로운 api호출과 함께 페이지가 reload되는 것이 일반적이었다. 

하지만 요즘 sns나 네이버와 같은 대형 플랫폼은 트렌드를 변화하고 있다. 이제 일반적으로 보이던 페이지네이션 바와 같은 ui는 그 자리를 잃어가고 있다.

사라져가고 있는 페이지네이션 ui

데이터가 넘쳐나고 그 넘쳐나는 데이터들 사이에서 자신이 원하는 정보를 빠르게 찾아냉하는 사용자들에게는 그 페이지가 깜빡하는 시간조차 아깝기 때문이다. 

이런 ux의 고민으로 적용하게 된 기술이 바로 무한 스크롤이다. 무한 스크롤은 따로 페이지 네이션을 통하지 않고 스크롤을 통해서 바로 바로 다음 데이터를 불러오게 된다. 

 

2. 어떻게 하려하는가?

나는 데이터 페칭 라이브러리로 react-Query를 사용하고 있다. react-Query는 강력한 기능이 많이 포함되어 있는데 그 중 하나가 바로 useInfiniteQuery이다. 검색하던 중 react-Query에서 무한 스크롤 기능으로 이 훅을 제공하는 것을 확인한 후 나는 이 훅을 사용해보기로 마음을 먹었다. 나는 express와 데이터 베이스는 mysql, ORM으로는 sequelize를 사용하기로 했다. 

 

3. 무엇이 필요할까?

간단하게 생각해봤다. 백엔드의 api조차 내가 구현하는 상황에서 무한 스크롤을 구현하려면 어떠한 데이터 속성이 필요할 지에 대해 고민해봤다. 

그 결과 내가 생각해 낸 결론은 끝을 알기 위해 전체 데이터의 수를 알아야할 거 같았다. 또한 한 번 스크롤 할 때 불러올 데이터의 수와 여태까지 데이터를 얼마나 불러왔는 지에 대한 정보를 알아야 여태까지 불러온 데이터의 다음 데이터를 불러올 수 있을 거 같았다. 따라서 express의 api를 설계할 때 다음과 같은 정보들이 포함되게 api를 설계했다. 

4. 실제 구현

- 백엔드 api

multiAlbumRouter.get('/getAllList', async (req, res) => {
	let offset = req.query.offset;//건너뛸 데이터의 수
	let limit = req.query.limit;//한 번에 보낼 데이터의 수
	const count = await db.Post.count();//Post 테이블의 전체 데이터의 수
	console.log(req.header);
	try {
		const postList = await db.Post.findAll({
			order: [['id', 'DESC']],
			offset: Number(offset),
			limit: Number(limit),
			//attributes: { include: [[sequelize.fn('COUNT', 'id'), 'totalPost']] },
			include: [
				{
					model: db.Image,
					attributes: ['src', 'PostId'],
				},
				{
					model: db.User,
					attributes: ['nickname', 'profileImage'],
				},
			],
		});
		const multiAlbum = {
			multiAlbumList: postList,
			hasMore: Number(offset) + Number(limit) < count ? true : false, // 끝을 알려주는 속성
			next: req.baseUrl + req.url,// 다음번에 호출할 api주소
			limit: limit, // 한번에 불러올 데이터의 수
			nextOffset: Number(offset) + Number(limit), // 다음 번에 건너뛸 데이터의 수 offset의 누적합
			count: count, // 테이블의 전체 데이터의 수 
		};
		res.status(200).json(multiAlbum);
	} catch (err) {
		console.log(err);
	}
});

따라서 내가 설계한 express의 api는 다음과 같다. 

다음과 같이 api를 설계하면 내가 아까 무한 스크롤을 구현할 때 필요할 거 같은 속성들을 모두 백엔드에서 제공해줄 수 있게 된다. 

 

- NextJS에서 useInfiniteQuery를 통한 무한 스크롤 구현 

나는 모든 데이터들을 불러오는 페이지의 컴포넌트 내부에서 useInfiniteQuery를 이용해 무한 스크롤을 구현하려고 했다. 

공식문서에 존재하는 useInfiniteQuery 사용방법은 다음과 같다. 

useInfiniteQuery의 사용법

읽어본 결과 useInfiniteQuery는 다음과 같은 기능을 제공한다. 

  • fetchNextPage - 다음 페이지를 불러올 때 사용하는 함수
  • fetchPreviousPage - 이전 페이지를 불러올 때 사용하는 함수
  • hasNextPage - 다음 페이지가 있는 지 판별하는 함수
  • hasPreviousPage - 이전 페이지가 있는 지 판별하는 함수
  • isFetchingNextPage - 다음 페이지를 불러오는 동안 로딩 상태
  • isFetchingPreciousPage - 이전 페이지를 불러오는 동안 로딩 상태

또한 queryKey에는 react-query 의 queryClient가 전역적으로 관리하는 데이터 query문의 고유한 키 값(이름)이 들어가야하고 pageParam에는 다음 페이지를 불러올 때 변수로 넘겨줄 변수가 할당된다. 

따라서 나는 이렇게 컴포넌트를 구성해보았다. 

const AlbumList = () => {
	const OFFSET = 4;
	const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteQuery(
		"getAllAlbumList",
		({ pageParam = 0 }) => // pageParam 초기 값을 넘겨줘야한다. 
			axios.get(`${API_HOST}/multiAlbum/getAllList`, {
				params: {
					limit: OFFSET, // 한 번에 4개의 데이터를 불러온다. 
					offset: pageParam,//초기 값 0이므로 테이블의 처음부터 데이터를 불러온다. 
				},
			}),
		{
			getNextPageParam: (lastPage) => {
				const { nextOffset, hasMore } = lastPage?.data;//가장 마지막에 불러온 페이지에 존재하는 데이터
				if (!hasMore) return false; // express api에서 넘겨준 hasMore속성 
				else {
					return Number(nextOffset);
				} // 다음 페이지를 불러오는 query문에 넘겨 줄 pageParam을 넘겨주는 함수이다. 
			}, // getNextPageParam에서는 리턴 값이 존재하거나 더 데이터가 없는 경우 false를 리턴한다. 
		},
	);

	const changeButtonText = (hasMore: boolean) => {
		return hasMore ? "load More ..." : "finish";
	}; // 만약 끝이라면 마지막에 존재하는 버튼에 들어가는 문구를 바꾼다. 

	const loadMore = () => {
		if (hasNextPage) {
			fetchNextPage();
		}
	};
	const AllAlbums = data?.pages;
	const onIntersect: IntersectionObserverCallback = ([{ isIntersecting }]) => {
		fetchNextPage();
	}; // 나중 포스트에서 작성할 InterservtionObserverCallback함수이다. 

	const { setTarget } = useIntersectionObserver({ onIntersect });
	return (
		<>
			<AlbumListComponentContainer>
				<AlbumListComponent>
					{status === "loading" && <div>loading...</div>}
					{status === "error" && <div>error</div>}
					{status === "success" &&
						AllAlbums?.map((page) => {
							return page.data.multiAlbumList.map((item: GetAllAlbumItemType) => (
								<AllAlbumCard
									key={item.Images[0].src}
									src={item.Images[0].src}
									title={item.title}
									content={item.content}
									nickname={item.User.nickname}
									linkUrl={`/multiAlbum/${item.id}`}
									profileImage={item.User.profileImage ? item.User.profileImage : blankProfile.src}
								/>
							));
						})}
					<FetchMoreButton onClick={loadMore}>
						{changeButtonText(data?.pages[data?.pages.length - 1].data.hasMore)}
					</FetchMoreButton>
				</AlbumListComponent>
			</AlbumListComponentContainer>
			<div ref={setTarget}></div>
		</>
	);
};

export default AlbumList;

다음과 같이 작성한 후 결과를 보면 스크롤을 할 때마다 다음 페이지를 불러와서 무한 스크롤을 구현할 수 있게 된다. 

원래 이렇게만 작성하면 스크롤 할 때마다가 아니라 밑에 FetchMoreButton을 누를 때마다 다음 페이지를 불러오게 되지만 다음 포스트에서 html태그가 뷰포트에 보일 때마다 로직을 수행할 수 있게 해주는 기능을 소개하려고 한다.

 

5. 결과

무한 스크롤

 

728x90
반응형

댓글