티스토리 뷰
개발냥이/타입스크립트(Typescript)
[Next.js + 타입스크립트] 무한 스크롤(Infinite scroll) 구현해보기(feat. Intersection observer)
데브캣_DevCat 2023. 7. 15. 12:02개요
- 무한스크롤을 구현하는 방법에는 스크롤의 높이와 클라이언트(화면)의 높이를 계산하는 스크롤 이벤트 기반 구현 방식도 있지만 (참고 사이트1,참고 사이트 2)
- 저는 IntersectionObserver 라는 WebAPI를 이용해서 무한스크롤을 구현해보았습니다.
- 스크롤 이벤트 기반 방식은 스크롤이 될 때마다 자바스크립트 엔진이 계속해서 함수를 호출하게 되는데
- Intersection observer는 브라우저단에서만 동작하고 필요할 때만 자바스크립트 코드를 불러오므로 성능상에서도 이점이 있고
- 무한 스크롤 외에도 여러 곳에서 사용할 여지가 있는 확장성이 있는 API라는 생각이 들었기 때문입니다!
동작 방식
- IntersectionObserver의 동작 방식은 매우 직관적이고 심플합니다.
- 관찰자(observer) 생성
- 관찰 대상(target) 생성
- 관찰 대상이 조건을 만족할 때 콜백 함수 실행
- 조건은 아래와 같은 3가지 경우입니다.
- 관찰 대상(target)이 등록됐을 때
- 대상이 화면에서 안보이다가 나타났을 때
- 대상이 화면에서 보이다가 사라졌을 때
// 1.
const observer = new IntersectionObserver(callback, options) => {}, options)
// 2.
observer.observe(target);
- 무한 스크롤을 구현할 때는 관찰 대상을 리스트 끝에 생성해두고 관찰 대상이 발견되었을 때(스크롤을 리스트 끝까지 내렸을 때) 새로운 데이터를 추가적으로 요청(callback)하면 되겠네요!
구현
useIntersectionObserver.tsx
const useIntersectionObserver = (callback: () => void) => {
const [observeTarget, setObserveTarget] = useState(null);
const observer = useRef<MutableRefObject<Element> | any>(null);
useEffect(() => {
observer.current = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting) return;
callback();
},
{ threshold: 1 },
);
});
- 먼저 observer를 생성하고 엔트리를 탐색합니다.
- 옵저버는 위와 같은 옵션을 갖습니다.
Options
- root : 기본값 null인 경우 브라우저의 뷰포트. 타겟의 가시성을 검사하기 위한 객체를 지정하는 값입니다
- rootMargin: root의 범위를 margin값을 이용해 확장하거나 축소할 수 있습니다.
- threshold: 옵저버가 탐지를 하기 위해 타겟의 가시성이 얼마나 필요한지를 나타낸 숫자입니다. 0인 경우 root가 타겟을 만나는 즉시 실행되고 1인 경우 타겟이 완전히 노출되었을 때 실행됩니다.
- entry는 IntersectionObserverEntry 인스턴스 배열인데 관찰하는 요소의 정보와 루트 요소의 정보가 들어있습니다.
Options
- boundingClientRect: 관찰 대상의 사각형 정보
- intersectionRect: 관찰 대상의 교차한 영역 정보
- intersectionRatio: 관찰 대상의 교차한 영역 백분율(intersectionRect 영역에서 boundingClientRect 영역까지 비율)
- isIntersecting: 관찰 대상의 교차 상태
- rootBounds: 지정한 루트 요소의 사각형 정보)
- target: 관찰 대상 요소
- time: 변경이 발생한 시간 정보
useIntersectionObserver.tsx
useEffect(() => {
const currentTarget = observeTarget;
const currentObserver = observer.current;
if (currentTarget) currentObserver.observe(currentTarget);
return () => {
if (currentTarget) {
currentObserver.unobserve(currentTarget);
}
};
}, [observeTarget]);
return setObserveTarget;
- observe는 관찰 대상을 관찰하기 시작함을 의미하고 unobserve는 관찰을 중지함을 의미합니다.
InfiniteScroll.tsx
const InfiniteScroll = () => {
const url = 'https://developer-lostark.game.onstove.com/markets/items';
const [fetchData, page, setPage] = useFetch(url, 'infinite');
const [observe, setObserve] = useState(false);
console.log(fetchData);
const pageHandler = () => {
setPage(page + 1);
};
useEffect(() => {
pageHandler();
setObserve(false);
}, [observe]);
const setObserveTarget: React.Dispatch<SetStateAction<any>> =
useIntersectionObserver(() => {
setObserve(true);
});
return (
<section>
<h2 className="text-3xl text-center text-orange-700 my-8">
infinite scroll page
</h2>
<DataList data={fetchData} usage={'infinite'} />
<div
ref={setObserveTarget}
style={{ backgroundColor: 'blue', padding: '10px' }}
/>
</section>
);
};
- 맨 아래에 div태그로 타겟을 지정해놓았습니다. 스타일을 준 것은 눈으로 타겟을 확인해보기 위해서입니다.
- 이제 데이터 리스트 아래에 놓인 타겟이 탐지되면 page+1을 한 후 그 값을 body에 넣어서 추가적인 API 요청을 하게 됩니다.
useFetch
type FetchReturnType = [
data: ItemsProps[],
page: number,
setPage: React.Dispatch<React.SetStateAction<number>>,
];
const useFetch = (url: string, usage: string): FetchReturnType => {
const [fetchData, setFetchData] = useState<ItemsProps[]>([]);
const [page, setPage] = useState(1);
const APIkey = process.env.NEXT_PUBLIC_LOSTARK_API_KEY;
useEffect(() => {
fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${APIkey}`,
'content-type': 'application/json;charset=UTF-8',
},
body: JSON.stringify({
Sort: 'RECENT_PRICE',
CategoryCode: 40000,
CharacterClass: '',
ItemTier: null,
ItemGrade: '',
ItemName: '',
PageNo: page,
SortCondition: 'ASC',
}),
})
.then((res) => res.json())
.then((data) =>
usage === 'pagination'
? setFetchData(data?.Items)
: setFetchData([...fetchData, ...data?.Items]),
);
}, [page]);
return [fetchData, page, setPage];
};
- 페이지네이션은 페이지가 넘어갔을 때 한 페이지당 정해진 데이터만 보여주면 되지만
- 무한 스크롤은 데이터가 점점 쌓여서 스크롤이 생기는 형식이기 때문에 데이터를 누적시켜주어야 합니다.
- 따라서 usage 에 따라 데이터를 다르게 저장하도록 하였습니다.
참고 자료
반응형
'개발냥이 > 타입스크립트(Typescript)' 카테고리의 다른 글
디바운싱(Debouncing)과 쓰로틀링(Throttling) 정리 (0) | 2023.08.19 |
---|---|
[리액트+타입스크립트] 프로젝트 리팩토링(refactoring)_사이드바(sidebar) (0) | 2023.07.31 |
[리액트+타입스크립트] 이미지 업로드 구현 & 이미지와 콘텐츠 하나의 객체로 관리하기 (0) | 2023.07.12 |
[Next.js + typescript] 페이지네이션(Pagination) 구현해보기 (0) | 2023.07.10 |
[리액트+타입스크립트] 경과 시간(날짜) 표시하기 & Intl API 알아보기 (0) | 2023.07.07 |
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 스프링부트
- 자바
- 알고리즘
- 정렬
- 프로그래머스
- JavaScript
- CS
- 이분탐색
- BFS
- Nest
- 자바bfs
- Algorithm
- 자바스크립트
- DP
- dfs
- Spring
- SQL
- JPA
- 자바트리
- 해시맵
- 자바dp
- 스프링
- 형변환
- Comparator
- java
- Queue
- 리액트
- SQLD
- 백준
- 타입스크립트
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함