728x90

구현코드

import { getUserHouseList } from '../../helper';

import { userKey } from '../../assets/constant/queryKey';
import BriefHouse from '../house/BriefHouse';

import { useInfiniteQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import styled from 'styled-components';
import { SearchHouse } from '../../types';

interface HouseListProps {
	search: string;
	select: string;
	type: string;
}

const HouseList: React.FC<HouseListProps> = ({ search, select, type }) => {
	const { isLoading, isFetching, isError, error, data, hasNextPage, fetchNextPage } = useInfiniteQuery<{
		data: SearchHouse[];
		totalCount: number;
	}>([userKey.userHouseList, search, type, select], ({ pageParam = 1 }) => getUserHouseList(search, type, select, pageParam), {
		getNextPageParam: (lastPage, pages) => {
			const maxPage = Math.ceil(lastPage.totalCount / 24);
			const nextPage = pages.length + 1;

			if (nextPage <= maxPage) {
				return nextPage;
			} else {
				return undefined;
			}
		},
	});

	useEffect(() => {
		const onScroll =  () => {
			const scrollY = window.scrollY;
			const innerHeight = window.innerHeight;
			const scrollMaxY = document.documentElement.scrollHeight - innerHeight;

			if (scrollY >= scrollMaxY - 250 && hasNextPage && !isFetching) {
				fetchNextPage();
			}
		};

		document.addEventListener('scroll', onScroll);

		return () => {
			document.removeEventListener('scroll', onScroll);
		};
	}, [fetchNextPage, hasNextPage, isFetching]);

	isError && console.log(error);

	
	return (
		<>
			{data !== undefined && data.pages[0].data.length > 0 ? (
				<>
					{data.pages !== undefined ? (
						<>
							{data.pages.map((group, idx) => (
								<ListWrapper key={idx}>
									{group.data.map((house) => (
										<SearchResultContents key={house.accomNumber}>
											<BriefHouse house={house} key={house.accomNumber} />
										</SearchResultContents>
									))}
								</ListWrapper>
							))}
						</>
					) : (
						<></>
					)}
				</>
			) : (
				<NonBox>등록된 숙소가 없습니다.😥</NonBox>
			)}
		</>
	);
};

export default HouseList;

const ListWrapper = styled.div`
	width: 100%;
	display: flex;
	flex-wrap: wrap;
	row-gap: 1rem;
	justify-items: flex-start;
`;

const SearchResultContents = styled.div`
	width: 25%;

	@media (min-width: 1020px) and (max-width: 1100px) {
		width: 33.3%;
	}

	@media (min-width: 500px) and (max-width: 1020px) {
		width: 50%;
	}

	@media (max-width: 500px) {
		width: 100%;
	}
`;
import { api } from '../api';
import { userUrl } from '../assets/constant';

const getUserHouseList = async (search: string, type: string, select: string, page: number) => {
	const resp = await api.get(userUrl.houseList, { params: { search, type, select, page } });
	return resp.data;
};

export { getUserHouseList };



예전부터 무한스크롤을 직접 구현해보고 싶은 욕망이 강했었습니다.

처음 무한스크롤에 대해서 관심을 가지게 된 건 오늘의집 무한스크롤이었습니다.
https://www.bucketplace.com/post/2020-09-10-%EC%98%A4%EB%8A%98%EC%9D%98%EC%A7%91-%EB%82%B4-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B0%9C%EB%B0%9C%EA%B8%B0/

 

오늘의집 내 무한 스크롤 개발기 - 오늘의집 블로그

무한 스크롤 적용 시 발생하는 문제점을 오늘의집 개발팀에서는 어떻게 해결했을까?

www.bucketplace.com

 

진짜 별 거 아닌 스크롤이라고 생각했는데, 제가 생각했던 것보다 훨씬 깊은 고민을 하였더군요...
그래서 기회가 되면 구현해보고 싶었습니다.

 

무한스크롤은 크게 옵저버 패턴과 스크롤 패턴으로 구분을 많이 하더군요

1. 옵저버 패턴 : 페이지 하단에 박스를 놔두고 이를 옵저버라고 합니다. 그리고 스크롤을 내리다가 해당 박스가 보이면 요청을 한다고 로직을 짜는게 옵저버 패턴입니다.

2. 스크롤 패턴 : 페이지가 특정 위치에 도달하게 되면 요청을 하는 겁니다. 보통 남은 페이지의 높이가 일정 이하일 때 요청한다고 로직을 짭니다.

 

아래는 오늘의집의 session-storage인데 스크롤 위치를 기억해서 랜더링을 하고 해당 위치로 이동한다고 블로그에 적혀있다.!

 

 

 현재 프로젝트 중이던 숙박앱에서도 요저가 숙소를 구경하고 나오게 될 텐데, 이때 스크롤 내역이 초기화된다면 굉장히 불편할 것이다. 따라서 해당 정보를 캐시해야하겠다고 생각을 했습니다.

 

이를 react-query를 이용하면 굉장히 쉽게 구현할 수 있습니다. (물론 어느 정도의 디테일을 보태냐따라 다르지만 단순히 해당 위치를 아는 건 쉽습니다)

https://tanstack.com/query/v4/docs/react/guides/infinite-queries

 

Infinite Queries | TanStack Query Docs

Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. TanStack Query supports a useful version of useQuery called useInfiniteQuery for querying these types of lists. When us

tanstack.com

 

우선 간단하게 로직을 생각해보면

1. 무한스크롤도 페이지일 뿐이다. 다만 페이지가 스크롤로 이동을 하고 이전 페이지 내용들이 계속 누적된다.

2. 이때 다음 스크롤로 페이지 이동을 어떻게 할 지를 작성해야 한다.

3. 이때 페이지 이동마다 요청했던 데이터를 계속 누적하는 로직을 쓰고, 이를 랜더링을 해줘야한다. 토한 요청시에 요청중 처리를 통해 한번에 하나의 요청만 하도록 한다.
4. 이 데이터를 전역변수 관리를 통해 상태관리 및 캐싱을 해서, 타 페이지에서 돌아와서도 유지되어야 한다.

이를 내가 구현한 순서에 따라서 다시 설명드리겠습니다.

 

#1 우선 백엔드와 api 통신을 통해서 해당 페이지에 대한 데이터를 받아오는 함수를 작성해야합니다.

(저희 조 백엔드 분들이 swagger를 통해 dx향상시켜주려고 노력하셔서 이렇게 보여드릴 수 있네요 ㅎㅎ..)

 

저희 페이지에서는 검색 결과를 무한슼롤로 받아오기에 type : 종류 select : 정렬순 , search :검색내용, page: 요청 페이지

이렇게 api가 구성되어있습니다. 저는 타입스크립트라서 타입을 추가적으로 지정했고, axiosIntance 이름이 api입니다.
 자바스크립트 였다면 
async (search, type, select, page) => {
const resp = await axios.get("http://백엔드주소" , {params : {매개변수}}; 
return resp.data;
};
이렇게 되었겠네요. return 값은 본인이 사용하기 편하게 가공하시면 됩니다.!

import { api } from '../api';
import { userUrl } from '../assets/constant';

const getUserHouseList = async (search: string, type: string, select: string, page: number) => {
	const resp = await api.get(userUrl.houseList, { params: { search, type, select, page } });
	return resp.data;
};

export { getUserHouseList };

이제 해당 함수를 useInfinite Queries를 통해서  사용해야겠네요

#2 훅을 통해서 통신 및 훅 설명

https://tanstack.com/query/v4/docs/react/guides/infinite-queries

더 자세한 설명은 있지만 저는 기본적인 것만 사용했고 각각의 원리를 설명하겠습니다. 

const { isLoading, isFetching, isError, error, data, hasNextPage, fetchNextPage } = useInfiniteQuery<{
		data: SearchHouse[];
		totalCount: number;
	}>([userKey.userHouseList, search, type, select], ({ pageParam = 1 }) => getUserHouseList(search, type, select, pageParam), {
		getNextPageParam: (lastPage, pages) => {
			const maxPage = Math.ceil(lastPage.totalCount / 24);
			const nextPage = pages.length + 1;

			if (nextPage <= maxPage) {
				return nextPage;
			} else {
				return undefined;
			}
		},
	});

 isLoading : 처음에 쿼리 로딩중인지 , isFetching : 요청함수가 실행중인지 , isError : 에러떴는지 , error  :어떤 에런지,

data : 통신 결과 데이터, hasNextPage : 다음페이지 있는지, fetchNextPage : 다음페이지 요청함수
이중에서 저는 hasNextPage와 fetchNextpage의 원리가 처음에 낯설었습니다.

fetchNextPage는 해당 함수가 실행되면  인피니티 쿼리훅은 두 번째 매개변수가 실행됩니다. ({ pageParam = 1 }) => getUserHouseList(search, type, select, pageParam)
이는 api 요청을 하는 함수지요.
즉, 페이지를 이동하다 다음페이지가 필요하면 해당 함수를 실행하면 됩니다.! 그럼 아래 함수가 실행되는 거죠

hasNextPage는 하단의 getNextPageParam을 통해서 결정이 되는데, 그전에 pageParam은 뭐하는 녀석이지? 라는 생각이 들겁니다.
   
이 두가지 값은  훅의 3번째 매개변수인 getNextPageParam에 의해 결정됩니다.
useInfiniteQuery를 통해 api 요청을 성공하면 데이터는 2가지로 나누어져서 저장됩니다.(매개변수 이름은 달라져도 됨)
lastPage  : 마지막 요청시 받은 데이터
pages : 데이터 요청시 받은 총 데이터로 배열에 각각의 페이지 요청시 받았던 데이터가 담겨 있습니다. pageParam 값을 통해서 구분되죠

구글링에서 예시들을 보면 백엔드에서 현재 너가 어떤 페이지에 대한 요청을 했는지 보내주는 경우도 있더군요. 하지만 저는 pages의 인덱스를 통해서 내가 몇번쨰 페이지를 요청했는지 계산했습니다.

 

maxPage는 백엔드에서 총 페이지 수 대신 총 데이터수를 보내주기 떄문에, 한번 요청당 24개를 받는 값을 나누어서 총 페이지를 계산했습니다.

nextPage는 pages의 인덱스를 통해서 내가 몇번째 페이지까지 요청했는지를 계산했습니다.

이때  현재 페이지 수 +1이 다음페이지입니다.

이후 총페이지수가 다음페이지보다 크거나 같으면 다음페이지로 넘어가고 없으면 넘어가지 않습니다.
이때 넘어간다를 구현하는게 getNextPageParam함수의 return 값입니다.
undefined 는 넘어갈 수 없다 이고 이럴 경우 hasNextPage 변수가 false가 됩니다.

return문에 값을 넣으면 pageParam 값이 해당 값으로 변경됩니다. 따라서 저는 1페이지 2페이지 넘어갈떄마다 pageParam 이 1 2 ~~~ 증가하겠죠

 

#3 useEffect를 통해서 이벤트 리스너 설정

useEffect(() => {
		const onScroll = () => {
			const scrollY = window.scrollY;
			const innerHeight = window.innerHeight;
			const scrollMaxY = document.documentElement.scrollHeight - innerHeight;

			if (scrollY >= scrollMaxY - 250 && hasNextPage && !isFetching) {
				fetchNextPage();
			}
		};

		document.addEventListener('scroll', onScroll);

		return () => {
			document.removeEventListener('scroll', onScroll);
		};
	}, [fetchNextPage, hasNextPage, isFetching]);

블로그 글을 쓰다 보니 구조가 아쉽네요.. 테스트 해봐야겠어요
우선 설명부터 하면 , onScroll이라는 함수를 통해서 페이지의 높이를 측정합니다. 

우선 scrollY 는 현재 내 스크롤 위치 , innerHeight 화면에 보여주고 있는 스크롤의 높이 그리고 document.documentElement.scrollHeight 는 총 스크롤의 높이입니다.

이를 통해서 현재 내 위치가 전체 높이 -250보다 크면. ( 남은 스크롤이 250px보다 작을떄) 요청을 합니다.
이때 다음페이지가 있고, 요청중이 아닐때만 요청을 해서 한번에 하나의 요청만 하도록 구현했습니다.

이런 함수를 document에 이벤트 등록시켜서 스크롤 이동시마다 실행되게 헀고, 새로 useEffect가 실행될 떄는 클린업 했습니다. https://ungumungum.tistory.com/46

 

리엑트 useEffect 훅의 역할 및 실행 순서 학습 및 실험

요약 useEfffect 내의 setupcode는 마운트와 업데이트 될 때 일반코드보단 늦게 실행된다.(마운트 이후 실행) useEffect 의 return문에 있는 cleanupcoded는 업데이트 될 때는 setupcode보다 먼저 , 언마운트 될 때

ungumungum.tistory.com

 

이제 데이터를 받아온걸 jsx 영역에서 뿌려주기만 하면 끝입니다.

	return (
		<>
			{data !== undefined && data.pages[0].data.length > 0 ? (
				<>
					{data.pages !== undefined ? (
						<>
							{data.pages.map((group, idx) => (
								<ListWrapper key={idx}>
									{group.data.map((house) => (
										<SearchResultContents key={house.accomNumber}>
											<BriefHouse house={house} key={house.accomNumber} />
										</SearchResultContents>
									))}
								</ListWrapper>
							))}
						</>
					) : (
						<></>
					)}
				</>
			) : (
				<NonBox>등록된 숙소가 없습니다.😥</NonBox>
			)}
		</>
	);

 

+ Recent posts