프로젝트를 진행하며 무한스크롤을 구현해야했다.
지금 구현되어 있는 코드는 다른 팀원의 내용과 여러 서치 결과를 섞어 급하게 짜놓은 코드이다. 결론적으로는 무한스크롤에 대해 100% 이해하지 못하고 짠 코드이다. 따라서 다시 코드를 살펴보며 왜 이러한 방법을 쓰게 되었을지
무한스크롤 자체에 대해 공부해보며 알아보려고 한다.
우선 무한 스크롤은 말 그대로 무한으로 스크롤 된다는 개념으로 생각하면 될 것 같다. 흔히 유튜브 등을 보면 바닥에 도달했을 때 새로운 데이터들을 불러오는 걸 볼 수 있다.

이것처럼 한 번에 모든 정보를 가져오는 것이 아닌, 스크롤을 통해 일부만 보여주고, 이후에 데이터를 추가로 가져오는 방식이 무한 스크롤 방식이다.
즉, 스크롤이 끝나는 지점에 도달하면 다시 데이터를 가져오는 것이다.
무한 스크롤을 구현할 수 있는 방법은 크게 2가지 정도가 있다.
우리가 평소에 많이 접해온 DOM의 Scroll Event를 사용하는 방식이다.
window.addEventListener('scroll',handleScroll);
스크롤이 발생할 때마다 이벤트를 실행하고,
현재 스크롤 위치를 계산해 바닥에 도달했는지를 판단한다.
const scrollTop=window.scrollY; // 현재 스크롤 위치const windowHeight=window.innerHeight; // 보이는 창 높이const fullHeight=document.documentElement.scrollHeight; // 전체 요소 높이if (scrollTop+windowHeight>=fullHeight) {
이 방식의 가장 큰 문제는 스크롤이 발생할 때마다 이벤트가 실행된다는 것이다.
이러한 문제를 해결하기 위해 보통 추가적인 방법을 고려한다.
쓰로틀링은 이벤트 실행 빈도를 제한하는 기법이다.
즉, 아무리 이벤트가 많이 발생해도 일정 시간에 한 번만 실행되도록 만든다.
이를 통해 불필요한 연산은 줄일 수 있지만, setTimeout 기반 throttle에는 한계가 있다.
→ 실제로 기대하고 있던 시간보다 더 뒤에 실행되는 문제 등이 발생할 수 있다.
위와 같은 문제를 보완하기 위한 대안 중 하나가 rAF 방식이다.
rAF는
즉, 렌더링 파이프라인과 동기화하여 실행할 수 있게 된다.
이렇게 되면
→ 결과적으로는 프레임당 한 번만 실행하게 된다.
따라서 더 안정적이고 불필요한 리플로우/리페인트를 줄일 수는 있다.
하지만, 스크롤 이벤트를 활용하는 방식 자체는 여전히 동일하다.
IntersectionObserver는 접근 방식 자체가 다르다고 느꼈다.
스크롤 이벤트를 활요하는 방식은 현재 스크롤이 어디에 있는가에 대한 계산이 무조건 필요했다.
하지만 IntersectionObserver는 이름 그대로
특정 요소(target)가 다른 요소(root)와 교차(intersect)했는지를 관찰(observer) 하는 API다.
우리가 JS로 직접 계산하던 것과 다르게,
브라우저가 엔진을 통해 특정 요소가 화면에 얼만큼 나오고 있는가를 판단하고, 그 결과를 우리가 받는 개념이 된다.
스크롤이 계속 일어나더라도, 관찰 대상(마지막 아이템)이 보이는 순간에만 함수가 실행된다.
IntersectionObserver 생성자는 두 개의 인자를 받는다.
const observer = new IntersectionObserver(callback, options);
(entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void
콜백 함수는 관찰 중인 요소의 교차 상태가 변경될 때 실행된다.
여기서 entries는?
{root?: Element | null; // 관찰 기준이 되는 요소로, null이면 viewport (브라우저 화면) 기준rootMargin?: string; // root 영역에 마진을 확장/축소 (뷰포트 기준 영역을 미리 늘려줌)threshold?: number | number[]; // 타겟이 얼마나 보여야 콜백 실행할지}
이제 이렇게 생성한 IntersectionObserver는 IntersectionObserver 인스턴스를 반환한다.
const observer = new IntersectionObserver(...)
이 객체에도 여러 메소드들이 있다.
observer.observe(targetElement);
observer.disconnect();
React에서 observer를 다시 생성할 때는 기존 observer를 반드시 disconnect() 해주는 것이 좋다.
아래는 실제 프로젝트에 적용되어 있는 코드 일부이다.
// 무한 스크롤 관찰자const observerRef = useRef<IntersectionObserver | null>(null);const lastItemRef = useCallback((node: HTMLDivElement | null) => {
브라우저는 메인 스레드이기 때문에 기존 스크롤 이벤트 방식은 관련 값 계산을 위해 계속 메인 스레드를 점유하게 된다.
IO 방식은 이와 다르게 브라우저의 별도 스레드(비동기)에서 이루어진다.
이전에는 이런 느낌이구나~ 하고 이해하고 넘겼던 것 같은데, 다른 어떤 방법들이 있고 어떤 문제들이 있었기에 지금 방식을 쓰는지 명확하게 이해할 수 있었다.
의식적으로라도 원초적인 방식들을 먼저 알아보고 사용하는 게 확실히 이해에도 도움이 되고 내 선택에 근거도 올바르게 만들 수 있는 것 같다.
→ 무한 스크롤에서는 entry.isIntersecting 를 통해 요소가 화면에 들어왔는지 판별하는 것!