무한 스크롤을 구현하려다 날밤새게 만든 Intersection Observer,
useEffect에서 무한히 생겨나던 fetch요청...
기억이 휘발되기 전 삽질의 흔적을 남겨보자 🙉
Intersection Observer를 사용해 구현된 CardListContainer
:
const CardListContainer = () => {
const [cardListInfo, setCardListInfo] = useState([]);
const [page, setPage] = useState(1);
const [io, setIo] = useState(null);
const fetchAPIData = async () => {
const data = await API.get.comments(page);
setCardListInfo([...cardListInfo, ...data]);
}
const registerObservingEl = (el) => {
io.observe(el);
}
useEffect(() => {
const targetIO = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setPage(page + 1);
if (io !== null) io.disconnect();
}
})
})
setIo(targetIO);
fetchAPIData();
}, [page])
return (
!cardListInfo.length
? <></>
: <CardListPresenter cardListInfo={cardListInfo} registerObservingEl={registerObservingEl} />
)
}
export default CardListContainer;
intersectionObserver의 인스턴스를 생성하고, 콜백함수로 관찰대상 요소가 들어올 경우 어떤 후속처리를 할 지 정의해준다.
관찰대상 DOM요소를 등록하는 함수를 정의한다.
const registerObservingEl = (el) => {
io.observe(el);
}
CardPresenter
컴포넌트까지 registerObservingEl
함수를 내려줌. useEffect(() => {
const targetIO = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
//다음 페이지의 card데이터를 fetch 해온다.
setPage(page + 1);
if (io !== null) io.disconnect();
}
})
})
setIo(targetIO);
fetchAPIData();
}, [page])
new
로 Intersection Observer의 인스턴스 객체를 생성하면, 이 객체는 관찰 대상 요소와 viewport와의 교차영역에 대한 변화를 비동기적으로 감지해준다.
(기본적으로는 viewport가 되지만 option으로 root를 설정해주면 이 또한 바꿀 수 있다)
이 때 전달해 줄 수 있는 인자는 2가지:
- callback 함수
- (선택) option 객체
const targetIO = new IntersectionObserver(entries => {
entries.forEach(entry => {
//후속처리 함수가 들어가는 자리
if (io !== null) io.disconnect();
}
}, {/* option객체 */})
위 코드의 callback함수가 받는 entries
는 어떻게 들어오는 걸까?
그래서 사용되는 것이 .observe()
메서드인 것!
생성된 Intersection Observer 인스턴스 객체는 편의상 io
라고 부르자.
io.observe(돔 요소);
를 하게되면 해당 돔 요소는 io
객체가 관찰하는 대상으로 등록이 된다.
const registerObservingEl = (el) => {
io.observe(el);
}
이번 과제에서는 'viewport에 카드리스트의 가장 마지막 카드가 들어오면 후다닥 다음 페이지의 API를 fetch해서 보여주자!'라는 것이 목표였던만큼,observe
메서드를 props로 전달해주기 위해 별도의 함수표현식으로 만들었다.
관찰을 멈추게 하는 메서드.
등록되었던 관찰대상과 io
와의 커넥션을 끊는다.
처음에는 useState()
를 이용해 상태로 io
객체를 관리하면 될 것이라 생각했다❗️
const [io, setIO] = useState(null);
당연히setIO
가 호출되고 리렌더링 되는 순간 앞전에 생겨난 io
객체가 garbage collecting되어 사라질꺼라 생각했지만 network탭을 통해 요청내역을 보니 그렇지가 않았다 😱
스크롤이 내려갈 때 뿐만 아니라 올라갈 때도 get요청이 가고 있다는 건 이전 렌더 때 생겨난 io
가 여전히 관찰을 이어가고 있다는 것! 없어지지 않았다는 사실 !!
그래서 #destroy intersection observer(스택오버플로우 참고) 라는 키워드로 검색을 해보니 io
객체를 삭제하는 방법 대신 disconnect
라는 메서드로 연결을 끊는 방법을 추천하고 있었다. 실제로 mdn 스펙상 destroy
메서드는 구현되지 않은 것을 확인할 수 있었다.
그래서 io
객체의 처리를 useState()의 상태로 다룰 때, 만들어진io
객체가 있다면(a.k.a 이전에 만들어진 io
객체) io.disconnect()
로 연결을 끊어주는 방식으로 변경했다.
const newIO = new IntersectionObserver(...);
...
if (io !== null) io.disconnect();
setIO(newIO);
이제는 스크롤을 올려도 get요청이 번복되지 않는다!!
😳 이번에야말로 최적화에 한 걸음 다가간 것이 아닐까 (흐뮷)
#Intersection Observer는 삭제말고 연결끊기
끝!
덕분에 버그 해결했습니다. 소중한 기록 감사합니다!