어떻게 구현해야 할지 전혀 감이 오지 않았던 주제라 구글링 시작.
Intersection Observer API를 사용했다.
현재까지 이해한 바, 교차 관찰자(=Intersection Observer)는 브라우저 창과 지정한 요소의 교차를 관찰한다.
그러니까 요소가 창에 들어오고 나갈 때를 감지하는 것.
들어올 때 IntersectionObserverEntry 객체를 반환하고 나갈 때도 반환한다.
그래서 리스트 마지막에 투명한 div
박스를 두고 교차 관찰자를 붙였다.
...
// 무한 스크롤
useEffect(() => {
const handleIntersection = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && hasNext) {
loadMessages({ recipientId, offset, limit: LIMIT });
}
});
};
const observer = new IntersectionObserver(handleIntersection, IO_OPTIONS);
observer.observe(SENTINEL.current);
return () => {
/* 페이지 전환 시 element 사라짐 대응 조건식 */
if (SENTINEL.current) {
observer.unobserve(SENTINEL.current);
}
};
}, [offset, hasNext]);
...
실패했을 때 코드를 기록해 두지 않아버렸다.
기억을 짚어보면 대충 원인이 몇 가지 있다.
처음에는 useEffect를 안 썼던 듯하다.
// 무한 스크롤
const handleIntersection = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && hasNext) {
loadMessages({ recipientId, offset, limit: LIMIT });
}
});
};
const observer = new IntersectionObserver(handleIntersection, IO_OPTIONS);
observer.observe(SENTINEL.current);
useEffect와 그 안의 클린업 함수를 쓰지 않고는 관찰 대상이 되는 html 요소에 붙였던 관찰자를 떼어낼 수가 없다.
즉, 리렌더링 될 때마다 인터섹션 옵저버가 매번 붙음으로서
요소가 화면에 교차될 때마다 한 번만 GET 요청을 해야 하는데 그 이상으로 요청하게 된다.
똑같은 데이터를 여러번 받아 오게 되는 것.
프로젝트는 오프셋 기반 페이지네이션 방식으로 데이터를 받아오도록 요구한다. 처음 로딩으로 id 0부터 5까지를 받아오면 그 다음 무한 스크롤을 통해서 6에서 11번 데이터를 한 번, 그 다음 12에서 17번을 받아와야 하는데
6-11번을 두 번,
12-17번을 세 번 받아오는 식의 문제가 발생했다.
useEffect
로 감싸기리렌더링 될 때마다 옵저버가 중복해서 등록되는 것을 막기 위해 useEffect를 사용.
다만 디펜던시 리스트에 뭐가 들어갈지 감이 안 잡혔다.
useEffect(() => {
// IO function
}, []);
디펜던시 리스트로 아무 값도 전달하지 않으면 loadMessages
함수의 아규먼트 값이 고정되어 버리는 문제가 있었다.
아규먼트로 전달한
{ recipientId, offset, limit }
의 값이 고정되어 버려서 스크롤 로딩을 할 때마다 매번 6-11번 데이터가 받아와지는 문제가 있었다.
파라미터 중 offset
의 값은 매번 바뀌어야 하는 값이었다.
[offset]
useEffect(() => {
// IO function
}, [offset]);
디펜던시 리스트로 offset의 값을 전달하게 되면 스크롤 로딩이 끝나고 바뀐 오프셋 값으로 인터섹션 옵저버(의 콜백함수)를 새로 생성한다.
처음엔
const handleIntersection = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadMessages({ recipientId, 6, limit: LIMIT });
}
});
};
였다가, 다음 로딩에는
const handleIntersection = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadMessages({ recipientId, 12, limit: LIMIT });
}
});
};
가 되는 것.
여기에 리렌더링 될 때마다 앞서 등록했던 옵저버를 해제하도록 클린업 함수를 등록했다.
useEffect(() => {
// IO function
return () => {
observer.unobserve(SENTINEL.current);
}
}, [offset]);
여기까지 왔을 때의 문제는 서버에서 마지막 데이터를 받아왔을 때 쿼리의 offset 값이 바뀌지 않고 useEffect 내부 코드도 더 이상 실행되지 않는다는 것이었다.
예를 들어 서버에서 받아온 데이터가 20-23번 네 개를 마지막으로 모두 받아오게 됐다면
더 이상 로딩이 되어야 하지 않는데 20-23번을 스크롤 할 때마다 리스트에 계속 붙이는 상태가 되어 버렸다.
그래서 리스폰스 객체에 있는 hasNext 프로퍼티를 디펜던시 리스트에 추가하고,
hasNext가 true일 때만 GET 요청을 보내도록 수정했다.
...
useEffect(() => {
const handleIntersection = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && hasNext) { // hasNext 조건 검사 추가
loadMessages({ recipientId, offset, limit: LIMIT });
}
});
};
...
교차를 감지하기 위해 만들었던 투명한 div 요소에
SENTINEL
이라는 useRef() 객체를 달아서 감시하고 있던 상태에서
페이지를 전환하게 되면 useEffect의 클린업 함수가 호출되는데,
이때 SENTINEL
이 참조하는 DOM 노드가 사라지고 그 값은 {current: null}
이 되어버린다.
그 상태에서 클린업 함수의 unobserve
메소드가 호출되면 에러를 일으킨다.
매개변수에 DOM 요소가 있어야 하는데 null이 있어서 인터섹션 옵저버를 해제할 수 없는 것.
그래서 요소가 존재할 때 클린업 함수를 실행하도록 조건식을 추가했다.
그 결과가 최종 결과이다.
// 무한 스크롤
useEffect(() => {
const handleIntersection = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && hasNext) {
loadMessages({ recipientId, offset, limit: LIMIT });
}
});
};
const observer = new IntersectionObserver(handleIntersection, IO_OPTIONS);
observer.observe(SENTINEL.current);
return () => {
/* 페이지 전환 시 element 사라짐 대응 조건식 */
if (SENTINEL.current) {
observer.unobserve(SENTINEL.current);
}
};
}, [offset, hasNext]);