1편에 이어서, Intersection Observer를 이용해 무한 스크롤을 구현하는 방법에 대해서 정리하려고 한다. 본격적으로 코드를 살펴보기에 앞서 Intersection Observer에서 다루고 있는 몇 가지 개념에 대해서 이해를 하고 넘어가야 한다.
// 타겟 요소 관측 시, 실행될 콜백 함수
const callback = (entries, observer) => {
console.log('콜백함수');
};
// Observer 선언
const observer = new IntersectionObserver(callback, options);
// 타겟 요소 관측 시작
observer.observe(TargetElement);
// 타겟 요소 관측 중단
observer.unobserve(TargetElement);
// 모든 요소 관측 중단
observer.disconnect();
// 관측 중인 모든 요소를 배열 형태로 반환
observer.takeRecords();
Intersection Observer 객체는 처음 생성될 때 타겟으로 지정한 요소 관측 시에 실행할 콜백 함수와 옵션 내용이 포함된 객체를 파라미터로 받는다. 옵션 객체는 문자 그대로 옵션이기 때문에 선택사항이며, 파라미터로 넘기지 않을 시 기본값이 적용된다.
파라미터로 넘긴 타겟 요소에 대한 관측을 담당한다. 동일한 옵저버 객체로 여러번 호출을 해서 다양한 타겟 요소에 대해 관측이 가능하다.
타겟 요소에 대한 관측을 중지하는 역할을 담당.
모든 타겟 요소에 대한 관측을 중지한다.
현재 관측 중인 모든 타겟 요소들을 배열 형태로 반환한다.
const option = {
root: null,
rootMargin: 0px, 0px, 0px, 0px,
threshold: 0,
}
default 값으로만 구성된 option 객체의 예시이다.
root(viewport) : 타겟 요소와 교차 영역을 정의하기 위해 사용하는 상위 요소 프로퍼티다. 만약 값을 넣지 않거나 null일 경우에는 브라우저의 viewport(보여지고 있는 요소)가 root로 지정된다.
rootMargin : root 요소에 적용되는 margin 값을 정의하기 위한 프로퍼티. 지정한 값만큼 교차 영역이 계산되며 루트의 범위가 축소하게 된다.
thresholds : 콜백함수를 실행시키기 위한 루트 영역과 타겟 요소와의 교차 영역 비율을 지정하는 프로퍼티.
0.0과 1.0 사이의 값으로 지정한다. 값이 0이라면 타겟 요소가 교차 영역에 진입했을 때를 의미하며, 0.5라면 타겟 요소의 절반이 교차 영역에 들어왔을 때, 1.0이라면 완전히 교차 영역에 진입했을 때 콜백 함수가 실행된다.
Intersection Observer API를 사용해 화면에 띄어준 콘텐츠 중에 맨 마지막 요소를 관측해 콜백함수를 실행하는 원리로 무한스크롤을 구현하게 된다.
타겟이 관측되었다면, 실행되는 콜백 함수 내에서 해당 요소에 대한 관측을 중지하고,
새로운 콘텐츠를 리스트에 추가한다. 그리고 다시 마지막 콘텐츠에 대해 관측을 시작하는 것이다.
❗️나는 가장 보편적으로 사용되는 방법으로
list
요소의 가장 아래에 빈div
요소를 생성하고, 그 요소에ref
를 달아주는 방법을 시용할 것이다. 이ref
를 통해서 교차시점을 확인할 수 있다.
const defaultOption = {
root: null,
threshold: 0.5,
rootMargin: '0px'
};
const observer = new IntersectionObserver(checkIntersect, {
...defaultOption,
...option
}
callback 함수 checkIntersect
는 target을 주시하는 역할을 할 것이고, 두번째 파라미터로defaultOptin
라는 객체값을 넘김으로써, 교차공간에 대한 프로퍼티를 전달해주었다.
return (
<Wrap>
<Nav>
...
</Nav>
<ul>
...
</ul>
{isLoaded && <p ref={setRef}>Loading...</p>}
</Wrap>
리스트 맨 아래에 관찰 대상을 만든다. 여기서 중요한 것은 ref={setRef}
이다.
const checkIntersect = useCallback(([entry], observer) => {
if (entry.isIntersecting) {
onIntersect(entry, observer);
}
}, []);
관찰 대상은 하나이므로 콜백함수의 인자로 들어오는 [entry]
와 observer
를 파라미터로 전달해주었다. 그리고 이 entry의 속성인 isIntersecting
을 이용해 조건을 검사하고, 조건에 해당하면 콜백 함수를 실행한다.
📌 하지만, 관찰자, 관찰 대상, 조건, 콜백 함수를 만들었지만 이대로 실행한다면 원하는 결과를 얻을 수 없다. 왜냐하면 관찰 대상은 새로운 데이터를 가져올때마다 변해야 하기 때문이다.
😇 리액트에서 의존배열의 값이 변할때마다 처리를 해주는 훌륭한 훅이 있으니, 바로 useEffect가 그것이다.
useEffect를 이용한 시나리오는 다음과 같다.
해당 시나리오를 진행할 로직은 다음과 같다.
useEffect(() => {
let observer; // (1)beserver 변수를 선언해주고
if (ref) { // (2) 관찰대상이 존재하는지 체크한다.
observer = new IntersectionObserver(checkIntersect, {
...defaultOption,
...option
}); // (3) 관찰대상이 존재한다면 관찰자를 생성한다.
observer.observe(ref); // (4) 관찰자에게 타겟을 지정한다.
}
return () => observer && observer.disconnect(); // 의존성에 포함된 값이 바뀔때 관찰을 중지한다.
}, [ref, option.root, option.threshold, option.rootMargin, checkIntersect]);
Intersection Observe는 오직 무한 스크롤을 구현하기 위한 개념이 아니다!
지연 로딩이나, 스켈레톤 UI를 구현할 때도 사용되기 때문에 로직을 분리해 사용한다면 프로잭트 곳곳에서
유용하게 사용할 수 있다.
import { useState, useEffect, useCallback } from 'react';
// 옵션 값을 지정한다.
const defaultOption = {
root: null,
threshold: 0.5,
rootMargin: '0px'
};
// 커스텀 훅 부분
// 관찰 대상을 지정할 수 있도록 ref값을 useState 훅을 이용해 state로 관리해준다.
// 관찰자를 만들어준다.
const useIntersect = (onIntersect, option) => {
const [ref, setRef] = useState(null);
const checkIntersect = useCallback(([entry], observer) => {
if (entry.isIntersecting) {
onIntersect(entry, observer);
}
}, []);
// 관찰자가 언제 관찰하는지, 관찰을 종료하는지에 대해 로직을 구현해준다.
useEffect(() => {
let observer;
if (ref) {
observer = new IntersectionObserver(checkIntersect, {
...defaultOption,
...option
});
observer.observe(ref);
}
return () => observer && observer.disconnect();
}, [ref, option.root, option.threshold, option.rootMargin, checkIntersect]);
return [ref, setRef];
}
export default useIntersect;
import useIntersect from '../utils/useIntersect';
const Lists = () => {
const dispatch = useDispatch();
const { apiData, isLoaded, pageCount } = useSelector(state => ({
apiData: state.apiData.data,
isLoaded: state.apiData.isLoaded,
pageCount: state.pageReducer.pageCount,
}));
const page = useRef(pageCount);
useEffect(() => {
dispatch(getDataFromApi(pageCount, true));
dispatch(getPageData(page.current));
}, []);
const [_, setRef] = useIntersect(async(entry, observer) => {
observer.unobserve(entry.target);
await dispatch(getPageData(page.current++));
await dispatch(getDataFromApi(page.current, true));
observer.observe(entry.target);
}, {});
return (
<Wrap>
<Nav>
...
</Nav>
<ul>
{apiData.map((item, idx) => {
...
})}
</ul>
{isLoaded && <p ref={setRef}>Loading...</p>}
</Wrap>
);
};
useEffect
를 사용해 데이터를 로드한다.isLoaded
상태 값을 바꿔준다.ref
로 잡아 isLoaded
상태 값에 따라 랜더링시켜준다.useEffect
를 사용해 타겟 요소의 상태 변경을 감지한다.useEffect
내에서 Intersection Observer 인스턴스를 생성한다.결과는 다음과 같다.
Intersection Observer를 사용했을 때
Scroll event와 쓰로틀링을 사용했을 때
Intersection Observer를 사용하면 reflow를 일으키지 않는다는 것만으로도 사용가치가 충분하다고 생각한다. 데이터 요청도 필요한 순간에만 이루어지고 있어 불필요한 캐싱을 방지하는데도 효과가 탁월했다.
막상 Intersection Observer를 사용하려고 했을때 숙지해야할 정보는 많았지만 훌륭하게 정보를 정리해준 글들이 많았기에 그리 어렵지 않게 코드를 작성할 수 있었다.
조금 더 쉽게 코드를 구현한 분들의 자료를 참조하면서 후에 한 번 더 리팩토링을 진행해봐야겠다.
이 글을 읽는 누군가가 Intersection Observer에 대한 부담감을 내려놓을 수 있을수 있기를 기대해본다. 😇
좋은 글 보고 프로젝트에 적용 시켰습니다☺️