요즘은 비대면 화상 관리 서비스 개발에 참여하고 있다. 그 중에서도 채팅 기능을 담당하게 됐는데, 채팅 무한 스크롤 기능을 구현하며 새롭게 배운 내용을 공유해보려고 한다.
Debounce와 Thorttle는 프론트엔드에서 이벤트 성능 최적화에 사용되는 기법 (이벤트 실행 빈도를 제어하는 기법)이다. 주로 사용자와 상호작용에 대한 이벤트를 효율적으로 처리하기 위해 사용하는데, 여기서 이벤트는 스크롤, 클릭, 키보드 입력 등이 있다.
Debounce는 이벤트 요청이 들어오고 일정 시간을 기다린 후 요청을 수행한다. (특정 시간동안 이벤트 수행이 발생하지 않으면 요청을 수행한다)
다시 정리하면, 사용자가 A이벤트를 발생시켰을 때, B시간(기다리는 시간) 타이머가 생성된다. B시간이 끝나기 전에 동일한 이벤트가 발생하게 된다면 이전 타이머를 제거한 후에 새로 타이머를 생성한다. 반대로, B시간 동안 이벤트가 발생하지 않는다면 이전 타이머는 제거되지 않고 콜백 함수가 실행된다. Debounce는 보통 자동 완성 검색 입력창을 구현할 때 사용한다.
실제 이런 방법을 학습 콘텐츠 개발에 적용해 본 경험이 있다. Canvas API를 사용하여 쓰기 판을 만들어 쓰인 글자를 인식하고, 그 데이터를 주기적으로 API를 통해 전송하고 결과를 받아와야 하는 기능이 있었다. 이 기능을 Debounce를 적용하여 API 호출이 너무 자주 일어나지 않게 개선해보았다.
Throttle는 사용자가 이벤트를 수행하는 동안 지정된 시간 간격으로 요청을 수행한다. 이벤트가 과도하게 발생할 때 Throttle는 성능 문제 해결에 도움을 준다. 대표적으로 무한 스크롤이나 리사이징을 구현할 때 자주 사용되는 기법 중 하나이다.
scroll
이벤트 문제점
scroll
이벤트는 짧은 시간동안 무수히 많은 이벤트가 동기적으로 실행 될 수 있는데 이로 인해 콜백 또한 무수히 실행될 수 있다. 결국 이러한 문제는 메인 스레드 성능 부하까지 발생시킬 수 있다.
가시성 관찰에 사용되는 getBoundingClientRect()
문제점
getBoundingClientRect()
는 요소의 크기와 위치 정보를 제공하는데, 주변 다른 요소들과의 관계 및 스타일, 레이아웃등을 고려하여 크기 및 위치를 계산해야한다. 이로 인해reflow
발생하게 된다.
IntersectionObserverEntry.boundingClientRect
getBoundingClientRect()
의 가장 큰 문제는 reflow
가 발생하게 된다는 것이다.
IntersectionObserverEntry.boundingClientRect
는 사용하면 요소의 가시 영역과 교차 여부 확인을 위해 사용하는 API인데, 이를 사용하면 다른 요소와 관계 고려하지 않고도 구할 수 있다.
특정 요소가 화면에 보이는지 보이지 않는지 판단하는 기능을 제공하는 API이다. 브라우저 뷰포트와 특정 요소의 교차점을 관찰하여 뷰포트에 요소의 포함 여부를 구별한다. Intersection Observer
는 모든 영역을 사각형으로 가정하고 교차성(가시성) 계산하고 있다.
우리는 tankstack-query
를 같이 사용했는데 useInfiniteQuery와 IntersectionObserver API 함께 사용하여 무한 스크롤을 구현하였다. 처음에는 테스트 코드를 작성하여 동작 구조를 확인하였다.
아래 훅을 채팅 메세지 목록을 위로 스크롤 했을 때(역방향) 새로운 데이터를 불러올 수 있게 구현하였다. tankstack-query
에서 useInfiniteQuery
를 사용했는데, hasNextPage
를 받아올 수 있다. 다음 데이터가 또 존재하는지 확인할 수 있는데 이에 따른 처리를 해주지 않으면 데이터를 가져오는 과정에 오류가 발생할 수 있다.
import { RefObject, useEffect, useRef } from 'react';
/**
* IntersectionObserver Hook
* @param targetRef 관찰 대상
* @param onIntersect 관찰 대상이 나타난 경우, 실행될 콜백 함수
* @param hasNextPage 다음 데이터 존재 여부
*/
export const useIntersectionObserver = <T extends HTMLElement>(targetRef: RefObject<T>, onIntersect: IntersectionObserverCallback, hasNextPage?: boolean) => {
const observer = useRef<IntersectionObserver | null>(null);
useEffect(() => {
if (!targetRef?.current) return;
observer.current = new IntersectionObserver(onIntersect, { root: null, rootMargin: '0px', threshold: 1.0 });
if (hasNextPage) {
observer.current.observe(targetRef.current);
}
return () => observer?.current?.disconnect();
}, [hasNextPage, onIntersect, targetRef]);
};
무한 스크롤 기능을 이전에도 다루어 본 경험이 있지만, 이번에 이렇게 공부하면서 적용해 본 방식과 비교해보면 흉내만 낸 것 같다는 생각이 들었다. 그래도 이번 경험을 통해서 여러 개념이나 동작 방식을 하나하나 구현해보면서 공부해 볼 수 있는 시간이었다.무지한 내 지식 바구니를 채울 수 있는 유익한 시간이었다. ꜆₍ᐢ˶•ᴗ•˶ᐢ₎꜆
참고
https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API