이전에 내가 진행했던 프로젝트에서 무한스크롤을 구현한적이있었다.
그 부분을 개선해보고자 글을 작성한다.
코드 수정 순서는
기본 scroll Event
=> Throttle
=> IntersectionObserver
순서로 비교를 해볼 예정이다.
const addDatas = useCallback(async () => {
try {
const filterCategory = makeCategory(category);
const item = await getCowDogInfo(person, dataIndex, filterCategory);
const temp = item.length === 0 ? makeDummyProfileData() : item;
setDatas([...datas, ...temp]);
setDataIndex((prev) => prev + 1);
} catch (e) {
setError((e as any).message);
}
}, [person, dataIndex, category]);
const onScroll = useCallback(() => {
const { scrollHeight, scrollTop, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight) addDatas();
}, [addDatas]);
useEffect(() => {
document.addEventListener("scroll", onScroll);
return () => {
document.removeEventListener("scroll", onScroll);
};
}, [dataIndex, category]);
위와 같은 코드로 진행을 하였다.
성능을 측정해보니
scroll event에 대한 콜백함수가 동작한다는 것을 확인할 수 있다.
그렇다, 위 함수가 addEventListener로 등록해준 onScroll 함수이다.
다음 사진은 1번의 api로 100개의 데이터를 갖고오는 api를 10번 요청했을 경우 성능 지표이다.
스크립트 2810ms
렌더링 144ms
페인팅 271ms
다음은 Throttle을 구현하여 적용해보았다.
export const useThrottle = (callback: () => void, time: number) => {
let timer: NodeJS.Timeout | null;
return () => {
if (!timer) {
timer = setTimeout(() => {
callback();
timer = null;
}, time);
}
};
};
const addDatas = useCallback(async () => {
try {
const filterCategory = makeCategory(category);
const item = await getCowDogInfo(person, dataIndex, filterCategory);
const temp = item.length === 0 ? makeDummyProfileData() : item;
setDatas([...datas, ...temp]);
setDataIndex((prev) => prev + 1);
} catch (e) {
setError((e as any).message);
}
}, [person, dataIndex, category]);
const onScroll = useCallback(() => {
const { scrollHeight, scrollTop, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight) addDatas();
}, [addDatas]);
const throttleScrollEvent = useCallback(useThrottle(onScroll, 300), [onScroll]);
useEffect(() => {
document.addEventListener("scroll", throttleScrollEvent);
return () => {
document.removeEventListener("scroll", throttleScrollEvent);
};
}, [throttleScrollEvent]);
throttle을 구현한것을 제외하고 코드가 달라진 것은 없다.
약 300ms 의 setTimeout을 적용해주었기에 scroll event 발동부터 300ms 이후 api 요청을 진행하게되었다.
하지만, 성능적인 부분에 이슈가 발생하였다.
스크립트 2874ms
렌더링 199ms
페인팅 251ms
오히려 기본 scrollEvent보다 성능이 더 좋지 못한 이슈가 발생하였다.
throttle을 사용하는 이유는 수 많은 scroll Event가 발생할때
지정한 시간동안 발생한 여러 요청을 단 한번의 요청으로 끝내기 위함이다.
그러기 위하여 setTimeout을 사용해주고있다.
하지만, 내가 구현한 scroll Event에는 조건을 만족하지 않으면 바로 return을 작성한다.
때문에, setTimeout을 사용하여 발생한 비용보다 height값 계산하는게 더욱 값이 싸기때문에
오히려 throttle을 사용하는 것이 bad case가 되었다.
그렇다면, scroll Event를 사용하는 것이 아니라, IntersectionObserver를 사용해보고 비교를 해보겠다.
위 이미지에서 보듯 scroll Event에 대한 콜백함수가 동작하지 않음을 알 수 있다.
분명 무수히 많은 scroll Event에 대해서 callback 함수가 발생했었는데,
그것이 작동안하니 훨씬 성능이 좋을 것이라 기대한다.
스크립트 2735ms
렌더링 193ms
페인팅 278ms
성능을 측정해보니
큰 차이를 가늠할 수 있는 정도는 아닌 것 같다.
[ 측정 환경 ]
API 1회당 100개 데이터
총 10번 API 요청
한 화면에 8개의 데이터가 view에 나타남
[ scroll Event ]
스크립트 2810ms
렌더링 144ms
페인팅 271ms
[ scroll Event Throttle ]
스크립트 2874ms
렌더링 199ms
페인팅 251ms
[ intersectionObserver ]
스크립트 2735ms
렌더링 193ms
페인팅 278ms
측정 동작과 측정 범위를 내가 직접 하다보니 오차가 발생할 수 있다고는 생각했다.
하지만, 성능의 차이가 이렇게 없을줄 몰랐는데,
내가 생각한 결론은 아래와 같다.
scroll Event로 인해 발생한 콜백함수의 동작이 무겁지 않아서 ( 높이 계산 후 종료 )
오히려 Throttle의 setTimeout이 더 무거워서 성능이 좋지 않았던 것 같다.
그러나, intersectionObserver의 경우 scroll event에 대한 콜백함수가 동작하지않아 스크립트 시간이 더욱 줄었다.
그러나, observer를 위한 div 태그를 그려주기때문에 페인팅 시간이 증가하였다.
무조건 이게 좋고, 이게 나쁘다가 아닌 것 같다.
scroll Event로 인해 동작하는 Callback 함수에 따라 달라진다.
나의 경우 동작하는 callback 함수가 매우 가벼웟을 뿐이라 생각한다.
무거운 callback 위 Url을 들어가면 scroll Event가 발생할때마다 reflow가 동작하는 코드를 확인할 수 있다.