티스토리 뷰
React에서 Infinite scroll 구현하기 (IntersectionObserver)
무한스크롤을 구현하는 방법은 다양하다!
- scroll
- IntersectionObserver
- getBoundingClientRect()
- useRef
이중에서 2번째, 웹 API로 있는 I-O를 사용해서 구현해보도록 할 것이다.
보기에 앞서 왜 IO를 사용하게 되었는지를 잠깐 정리해본다.
scroll
스크롤이벤트를 attachment하여 현재 스크롤위치를 계산해서
맨 끝에 스크롤되었을 때 다음 데이터를 fetch하는 방법
을 사용한다.
스크롤마다 한번의 이벤트가 발생하므로(거의 픽셀단위) 성능이슈를 예방하기 위해 쓰로틀링
적용을 고려해야한다.
추가적인 정보는 아래 포스팅에서 확인할 수 있다.
스크롤로 구현하기
getBoundingClientRect()
스크롤방식처럼 1픽셀 1이벤트 + 쓰로틀링을 구현하지 않고 같은 기능을 구현할 수 있다.
위치를 계산하는건 동일하지만 사각형 영역을 계산해 요소가 어디 위치해있는지를 확인하는 방법으로 `getBoundingClientRect()` 메소드를 사용한다
Infinite Scroll에 적용해본다면, 특정요소가 viewport
에 존재하는지 판단해 데이터 fetch를 결정할 수 있다.
여기서 viewPort란 사용자가 보고있는 페이지의 영역이다.
따라서 getBoundingClientRect는, 현재 보고있는 영역내에 이벤트를 트리거할 요소가 존재하는지 확인하기 위한 위치연산 함수인 것이다.
그러나 이 과정에서 reflow가 발생하여 스크롤로 구현하는 방법과 마찬가지로 성능이슈가 생길 수 있다.
요소의 위치, 크기를 계산하고 위치시키는 reflow를 수행하지 않는 IntersectionObserver
를 사용함으로써 이러한 문제를 해결할 수 있다.
IntersectionObserver
viewport
와 특정 요소가 교차하는지를 관찰하여 특정 %이상겹쳐졌을 때 겹쳐졌음을 감지하는 기능을 제공하는 웹 API다.
사용자가 보고있는 영역이 빨간색이고 이벤트를 트리거할 요소가 하단의 InterSectRef라고 했을 때,
50%겹쳐졌을 때 이벤트를 트리거한다고 작성을 했다면 아래 그림의 시점에서 새로운 데이터를 불러오는 요청을 보내도록 구현할 수 있다.
매우 간단하게 !
구현 방법은 다음과 같다.
먼저 초기화
어떤 요소를 계속 관찰해야하기 때문에 observer를 생성해주어야한다.
이 옵저버를 초기화할 때 options
인자를 전달하는데,
options
은 특정요소와 viewport가 얼마만큼 겹쳤을 때 겹쳤다고 할지와 관찰대상의 부모요소를 지정하는 객체이다.
threshold가 바로 영역의 %를 뜻하는 프로퍼티이다. 0.5면, 50%겹쳐있을 때 콜백이 실행된다.
가장 중요한 것은 '무엇을' 관찰할지 인데 Js에서는 dom 선택을 사용해 할 수 있지만, 리액트는 load이전까지 html이 비어있으므로
useRef를 사용해 dom을 선택하도록 한다.
observe 메소드를 사용해 관찰할 target dom을 지정해서 영역 겹침 감지를 시작하도록 하자.
이를 코드로 구현하면 다음과 같다.
const options = {
root: null, //기본 null, 관찰대상의 부모요소를 지정
rootMargin: "20px", // 관찰하는 뷰포트의 마진 지정
threshold: 1.0, // 관찰요소와 얼만큼 겹쳤을 때 콜백을 수행하도록 지정하는 요소
};
useEffect(() => {
const observer = new IntersectionObserver(handleObserver, options);
if (interSectRef.current) observer.observe(interSectRef.current);
return () => observer.disconnect();
}, [handleObserver]);
interSectRef
요소를 관찰할 것이며 viewport와 1.0(100%) 겹쳐졌을 때 겹쳐짐이 true로 set될 것이다.
options
로 초기화, target을 관찰하기 시작했다면 다음은 교차되었을 때 특정 행위를 수행할 콜백을 작성하는 것이다.
위의 코드에서 콜백은 handlerObserver
이며 이는entries
라는 배열인자를 받는다.
IntersectionObserver
은 비동기적으로 수행되므로useEffect
에서 clean up에disconnect
를 사용해 관찰을 중지시킬 수 있도록 한다.
entires
observer는 하나 또는 여러 대상들을 관찰할 수 있는데 관찰타겟을 entries라는 배열로 받는다.
관찰대상을 여러개 지정했으면 각 요소의 정보들이 담겨있다. -> 무슨 태그고,, 어느위치에 있고 현재 겹쳐져있는지,,, 등등
observe
의 대상이 intersectRef
하나이기에 0번째 요소가 관찰 대상이된다.
콜백
threshold가 한 방향 그리고 다른 방향으로 교차될 때 실행된다. (위에서 아래, 아래에서 위, 혹은 좌 -> 우, 우->좌 일때를 얘기하는 듯함)
const handleObserver = useCallback(async (entries) => {
const target = entries[0];
if (target.isIntersecting) {
console.log("is InterSecting");
setPage((prev) => prev + 1);
}
}, []);
target
과 뷰포트가 1.0, 즉 100% 겹쳐졌을 때 isIntersecting
이 true로 되고 if조건에 만족해 다음 페이지를 불러오도록 state값을 더했다.
useEffect(async () => {
await fetchData(page);
}, [page]);
교차될 때마다 page값을 증가시켜 useEffect
가 호출되어 다음페이지에 대한 요청을 호출한다.
생각했던 것 보다 구현에 있어서 어려움이 크게 있진 않았다.
다만 발생할 수 있는 문제점(비동기함수 clean up)등을 잘 체크하며 구현하는 것이 중요하다고 생각된다..
발생했던 이슈
내가 예제로 사용했던 api는 여러정보를 불러오는 것이 아닌 한번에 하나의 데이터만 불러오는 api였다.
그래서 연습삼아 5개의 요청 수행해 5개씩 받아오도록 하는 코드를 작성했었다. (실제로 이러진 않겠지만)
첫번째로 반복문을 통해 5개의 요청을 보내 데이터를 쌓으려고 했었다.
const getNextData = async () => {
const dataContainer = [];
try {
for (let i = id; i <= id + 5; i++) {
const getFiveData = async (i) => {
const nextData = await fetchData(i);
if (nextData !== undefined) {
console.log("container push", i, nextData);
dataContainer.push(nextData);
}
};
getFiveData(i);
}
setData([...data, ...dataContainer]);
} catch {
console.log("NOT FOUND");
}
};
실제로 쌓이는 값은 결과의 마지막, 5번째의 요청만 쌓이고 있었다.
setState
는 비동기로 호출되어 마지막값만 반영된다는 것을 확인하고 다른 방법을 택하기로 했다.
promise all
const getNextData = async (id) => {
const promiseContainer = [];
try {
for (let i = id; i < id + 5; i++) {
promiseContainer.push(fetchData(i));
}
Promise.all(promiseContainer).then((value) => {
value = value.filter((v) => v !== undefined);
setData([...data, ...value]);
});
} catch {
console.log("NOT FOUND");
}
};
각 promise를 배열에 담아 promise.all
를 사용해 모든 요청에 대한 결과를 쌓도록 변경해 원하는 결과를 얻을 수 있었다.
하지만 요청이 실패하는 경우 (데이터가 없을 때)엔 다음 page에 대한 요청을 해야하는데 그건 for문으로 처리할 수 없었다.
결국 한개의 요청을 보내기로 하고 다음과 같이 코드를 변경했다.
single Request
useEffect(async () => {
await fetchData(page);
}, [page]);
const fetchData = useCallback(
async (nextPage) => {
try {
const res = await axios.get(
`${API_ENDPOINT}/${nextPage}?api_key=${API_KEY}`
);
setData([...data].concat({ ...res.data }));
return res.data;
} catch {
console.log("ERROR");
setPage((prev) => prev + 1);
}
},
[data]
);
요청이 실패할 경우 setPage
로 다음 페이지를 set하여 요청을 다시보내도록 변경했고 없는 데이터가 있을 때 다음 page에 대한 데이터를 받아올 수 있도록 처리하였다.
결과
참고
'프로그래밍 > React' 카테고리의 다른 글
Drag n Drop 기능 구현하기 + 애니메이션 (2) | 2021.08.28 |
---|---|
Infinite Scroll 구현 (feat. pair Programming) (2) | 2021.07.27 |
Styled-Components With TS(theme, globalStyle) (0) | 2021.06.12 |
recoil 정리 (0) | 2021.05.25 |
Scroll Restoration (0) | 2021.04.30 |
- Total
- Today
- Yesterday