안녕하세요!
우리가 매일 사용하는 인스타그램이나 유튜브처럼 화면을 아래로 내릴 때마다 끊임없이 새로운 콘텐츠가 나타나는 기능을 보신 적 있으시죠? 오늘은 이처럼 매끄러운 사용자 경험을 제공하는 인피니트 스크롤을 외부 라이브러리 없이 순수 자바스크립트만으로 구현하는 방법을 심도 있게 다뤄보려고 합니다. 특히 단순히 기능만 돌아가는 수준을 넘어, 브라우저의 부하를 최소화하는 성능 최적화 관점에서 어떻게 설계해야 하는지 저의 시행착오와 함께 공유해 드릴게요.

1. 기존 방식의 한계: 왜 Scroll 이벤트는 위험할까?
처음 인피니트 스크롤을 구상했을 때, 제가 가장 먼저 떠올린 방법은 window.addEventListener(‘scroll’, …)였습니다. 현재 스크롤 위치가 문서 전체 높이에 도달했는지를 계산하는 아주 직관적인 방식이죠. 하지만 이 방식에는 치명적인 단점이 있었습니다.
스크롤 이벤트는 사용자가 마우스 휠을 살짝만 움직여도 수백 번씩 발생합니다. 그때마다 자바스크립트 엔진이 현재 위치를 계산하고 DOM을 건드린다면, 저사양 기기에서는 화면이 뚝뚝 끊기는 ‘정크(Jank)’ 현상이 발생하게 됩니다. 저 역시 처음엔 이 방식으로 구현했다가, 스크롤이 내려갈수록 팬 소음이 커지는 노트북을 보며 “이건 아니다” 싶은 생각이 들더라고요.
이때 대안으로 찾은 것이 바로 브라우저 내장 API인 Intersection Observer입니다.
2. Intersection Observer API를 활용한 인피니트 스크롤 최적화
이 API의 핵심은 “스크롤 위치를 계산하지 않는다”는 점에 있습니다. 대신 우리가 지정한 특정 요소(예: 페이지 최하단의 빈 div)가 브라우저의 화면(Viewport)에 들어왔는지를 브라우저가 직접 감시하게 합니다.
이 방식은 메인 스레드에 부담을 주지 않으면서도 아주 정확하게 페이지 하단 도달 시점을 알려줍니다. 제가 이번 프로젝트에서 가장 감탄했던 부분이기도 한데요, 성능과 가독성 두 마리 토끼를 모두 잡을 수 있는 정석이라고 볼 수 있습니다.
3. 핵심 로직 구현: 화면의 끝을 감지하는 법
프로젝트를 진행하며 가장 공을 들인 부분은 역시 “어떤 시점에 다음 데이터를 불러올 것인가”였습니다. 저는 페이지의 가장 하단에 sentinel(감시자)이라는 빈 요소를 하나 두고, 브라우저가 이 요소를 발견하는 순간을 포착하도록 설계했습니다.
이때 사용한 핵심 코드는 다음과 같습니다.
// 감시자(sentinel)가 화면에 나타날 때 실행할 콜백 함수
const handleIntersect = (entries, observer) => {
entries.forEach(entry => {
// 요소가 화면에 10% 이상 들어왔을 때만 실행
if (entry.isIntersecting && !isLoading) {
fetchNextPage(); // 다음 데이터를 가져오는 함수 호출
}
});
};
// Intersection Observer 인스턴스 생성
const observer = new IntersectionObserver(handleIntersect, {
root: null, // 브라우저 뷰포트를 기준으로 감시
rootMargin: '0px',
threshold: 0.1 // 10% 지점이 교차될 때 반응
});
// 감시 시작
const sentinel = document.querySelector('#sentinel');
observer.observe(sentinel);
이 로직을 짤 때 가장 큰 수확은 isIntersecting이라는 속성을 활용해 불필요한 계산을 줄인 것이었습니다. 이전의 스톱워치 프로젝트나 계산기 구현 때처럼 복잡한 수학 연산을 매번 수행할 필요 없이, 브라우저가 “지금 보입니다!”라고 신호를 줄 때만 움직이면 되니까요. 덕분에 메인 스레드는 다른 작업을 처리할 여유를 갖게 되고, 결과적으로 사용자에게 끊김 없는 경험을 선사할 수 있게 되었습니다.
또한 isLoading이라는 상태 변수를 두어, 데이터를 가져오는 도중 중복으로 함수가 호출되는 ‘더블 페칭(Double Fetching)’ 현상을 막는 디테일도 잊지 않았습니다. 이런 사소한 방어 코드가 실제 서비스의 안정성을 결정짓는 중요한 요소가 되곤 하거든요.
4. 데이터 구조와 렌더링 로직 설계
실제 실무 환경이라면 외부 API에서 데이터를 가져오겠지만, 이번 실습에서는 100개의 가상 데이터를 배열로 만들어 10개씩 끊어서 보여주는 로직을 짰습니다. 데이터를 한꺼번에 렌더링하지 않고 ‘필요할 때만’ 추가하는 것이 포인트입니다.
시행착오 공유:
데이터를 추가할 때 처음에는 innerHTML += … 방식을 썼습니다. 그런데 데이터가 많아질수록 기존 리스트를 통째로 다시 그리느라 속도가 현저히 느려지더군요. 그래서 DocumentFragment를 활용해 메모리상에서 리스트를 먼저 완성한 뒤, 한 번에 DOM에 붙이는 방식을 택했습니다. 이 기법은 제가 이전에 다루었던 [이벤트 위임 패턴으로 최적화한 자바스크립트 OX 퀴즈 게임] 포스팅에서도 강조했던 효율적인 DOM 조작법과 일맥상통합니다.
5. 실전 코드 구현 및 인터랙티브 UI

특히 아이템이 추가될 때 갑자기 툭 튀어나오지 않도록 CSS의 opacity 애니메이션을 곁들였습니다. 자바스크립트 로직만큼이나 중요한 게 사용자에게 주는 부드러운 인상이니까요. 이 과정에서 사용한 배열 처리 기술은 [배열 고차 함수(reduce)로 만드는 자산 관리 자바스크립트 가계부]에서 사용했던 데이터 가공 로직을 응용하여 훨씬 깔끔하게 정리할 수 있었습니다.
6. [심화] 한 걸음 더: 스로틀링과 메모리 관리
만약 여러분이 더 복잡한 환경에서 인피니트 스크롤을 운영한다면, 메모리 관리에도 신경을 써야 합니다. 데이터가 수천 개, 수만 개가 쌓이면 브라우저는 결국 무거워질 수밖에 없습니다.
이럴 때는 화면에서 사라진 위쪽 요소들을 잠시 제거했다가 다시 스크롤이 올라올 때 그려주는 ‘가상 리스트(Virtual List)’ 개념을 도입해 볼 수 있습니다. 비록 이번 실습에서는 다루지 않았지만, 성능에 진심인 개발자라면 꼭 한 번 파고들어 볼 만한 주제입니다. 마치 제가 스톱워치를 만들 때 정밀도와 성능 사이에서 고민했던 것처럼, 개발의 끝은 결국 디테일한 최적화에 있는 것 같습니다.
오늘 구현해 본 인피니트 스크롤은 단순한 기능을 넘어 웹 성능과 UX를 동시에 고려해야 하는 깊이 있는 주제였습니다. 라이브러리를 쓰면 단 몇 줄이면 끝나겠지만, 이렇게 직접 원리를 파악하고 최신 API를 적용해 보는 과정이 우리를 더 단단한 개발자로 만들어준다고 믿습니다.
단순히 기능을 만드는 것에 만족하지 않고, “어떻게 하면 브라우저가 덜 힘들게 일하게 할까?”를 고민하는 습관이 쌓이다 보면 어느새 실력이 훌륭한 개발자가 되어 있을 거예요. 여러분의 프로젝트에도 오늘 배운 최적화 기법을 꼭 적용해 보시길 바랍니다.
코드를 작성하시다가 잘 안 풀리는 부분이나, 더 효율적인 로직이 떠오르신다면 언제든 의견 나눠주세요!