사실 이전 프로젝트에서도 useInfiniteQuery 기반으로 무한 스크롤을 구현했지만,
트리거 방식으로는 외부 라이브러리(useInView)를 사용 하였었다.
마침 맡은 파트가 두 개의 리스트(체험 관리 & 예약 내역)로 구성되어 있었기 때문에, 각 리스트에 서로 다른 방식 (useInfiniteQuery + useInView / useInfiniteQuery + useIntersectionObserver ) 을 적용해보고 두 트리거 방식 간 실제 구현 난이도와 재사용성 차이를 체험해보고 싶었다.


구현 목표
무한 스크롤(Infinite Scroll)은 무엇인가?
무한 스크롤은 사용자가 페이지를 스크롤할 때마다 자동으로 다음 콘텐츠를 불러오는 UI/UX 패턴입니다. 대표적으로 페이스북, 인스타그램, 트위터 등의 SNS 피드, 상품 목록, 블로그 글 목록 등에서 흔히 볼 수 있다.
▪ 탐색 흐름 유지: 클릭 없이 계속 콘텐츠를 탐색할 수 있어 몰입감을 준다.
▪ 페이지 전환 부담 감소: ‘다음 페이지로 넘어가는 로딩’이라는 행동이 사라져 사용자 행동이 간단해짐.
▪ 모바일 친화적: 모바일 환경에서 탭 클릭보다 스크롤 기반 탐색이 더 자연스럽고 익숙함.
생각해보면 과거에는 대부분 페이지네이션을 사용 한거 같은데 어느 순간 무한 스크롤이 조금씩 보이기 시작했다. 회고 작성하면서 궁금해서 찾아 보니 2006년~2010년 사이, 페이스북과 트위터가 모바일을 중심으로 성장하면서 사용자 경험 중심의 UI가 주목받기 시작. 그 중심에 있던 기능 중 하나가 바로 무한 스크롤이라고 한다. 여러모로 페이스북이 리액트 개발도 그렇고 많은 변화를 일으킨거 같다.
① 사용자가 지정된 트리거 조건에 도달 (예: 하단 스크롤, 요소 감지, 버튼 클릭 등)
② fetchNextPage()로 새로운 데이터 요청
③ 기존 리스트 뒤에 데이터를 이어 붙임
1. JavaScript 기본 방식 (스크롤 이벤트 기반)
scroll 이벤트를 직접 감지하여 window.innerHeight + scrollY >=document.body.offsetHeight 조건이 참이 되면 데이터 로딩
장점
▪ 별도 라이브러리 필요 없음
▪ 모든 브라우저에서 기본 지원
단점
▪ 성능 최적화 필수 (throttle, debounce 등)
▪ 이벤트 정리 누락 시 (정리 함수 필수) 메모리 누수 가능
▪ 특정 요소 관찰 어려움 (viewport 하단 기준만 가능)
2. useInfiniteQuery 방식
페이지 단위로 데이터를 불러오고, 사용자가 요청할 때마다 다음 페이지를 가져오는 기능을 제공하는 React Query 훅으로 기존의 useQuery가 단일 요청을 담당했다면, useInfiniteQuery는 다음 페이지(next page)를 기준으로 데이터를 누적하여 가져올 수 있게 설계
장점
▪ 커서 기반 페이징에 최적화 → pageParam을 통해 커서 기반 또는 페이지 기반 페이징 모두 유연하게 대응 가능
▪ 페이지 단위 캐싱 → 이전에 가져온 각 페이지 데이터가 캐시에 남아 있어, 새로고침 시에도 빠르게 복구됨
▪ 스크롤 트리거와 분리 가능 → fetchNextPage()는 외부에서 수동으로 호출 가능하므로 IntersectionObserver, 버튼 클릭 등 다양한 트리거와 조합 가능
▪ 데이터 누적 처리 자동화 → data.pages로 페이지별 데이터를 누적하여 관리할 수 있어 리스트 UI 구현이 간편
단점
▪ React Query 내부 흐름 이해 필요 → pageParam, getNextPageParam, 쿼리 캐싱 구조에 대한 이해가 없으면 구현이 어려울 수 있음
▪ getNextPageParam 작성이 번거로움 → API 응답 구조에 따라 다음 페이지 커서를 추출하는 로직을 매번 직접 정의해야 함
▪ 스크롤 감지 기능은 포함돼 있지 않음 → 무한 스크롤을 위해선 IntersectionObserver 또는 useInView 등 별도 감지 로직과의 조합이 필요
커스텀 훅 useIntersectionObserver 방식
브라우저의 IntersectionObserver API를 이용해 요소가 보이는 시점에만 fetchNextPage() 호출
장점
▪ 성능 우수 (브라우저 네이티브 관찰 API)
▪ 원하는 요소에만 반응 → 유연한 구성 가능
▪ 비즈니스 로직 커스터마이징 용이
단점
▪ API 학습 필요(IntersectionObserver라는 브라우저 API의 작동 방식, 옵션, 메서드를 새로 배워야 함)
▪ 직접 ref 관리 및 로직 작성 필요 (관찰 대상 요소를 직접 ref로 연결하고, observer 생성/해제 타이밍까지 제어해야 하므로, 초보자에게는 진입장벽이 느껴질 수 있다. 초보자에겐 진입장벽)
react-intersection-observer 라이브러리의 useInView 훅을 사용
장점
▪ 코드가 간결함 (최소 코드로 빠른 적용)
▪ ref, inView 분리로 제어 명확
▪ 커스텀 IntersectionObserver 옵션 지원
단점
▪ 내부 동작 추상화돼 있어 디버깅 어려움
▪ 특수한 조건에 대응 어려움 (다중 대상, 조건 분기 등)
useInView vs useIntersectionObserver| 항목 | useInView | useIntersectionObserver (커스텀 훅) |
|---|---|---|
| 구성 복잡도 | 낮음 (ref, inView 1줄로 사용 가능) | 중간 (ref 수동 관리 + observer 직접 작성 필요) |
| 제어 유연성 | 낮음 (라이브러리 내부 로직 추상화됨) | 높음 (옵션 및 콜백 커스터마이징 가능) |
| 코드 간결성 | 매우 간결 | 다소 복잡 |
| 재사용성 | 높음 (여러 컴포넌트에서 그대로 사용 가능) | 높음 (비즈니스 로직 포함한 커스텀 확장 가능) |
| 디버깅 용이성 | 다소 어려움 (내부 로직 접근 어려움) | 쉬움 (직접 작성한 코드로 흐름 추적 가능) |
| React Query 연동 | 자연스럽게 연동 가능 | 자연스럽게 연동 가능 |
| 에러 대응 유연성 | 낮음 (라이브러리 한계) | 높음 (observerCallback 커스터마이징 가능) |
비교 결과
① useInView는 정말 빠르게 구현 가능해서, 간단한 무한 스크롤에는 탁월하지만, 관찰 조건을 조금 더 정밀하게 다루려면 직접 구현한 useIntersectionObserver가 훨씬 유연
② 단순히 페이지 끝에서 fetchNextPage()를 호출하는 구조라면 useInView가 편리하지만, 조건이 많거나 같은 로직을 다른 리스트에도 적용할 땐 커스텀 훅이 훨씬 구조적으로 깔끔하고 유지보수에 이점이 있음 (일반적으로는 useInView로 충분할 거 같다는 생각이 든다. )
③ 개발 중에 스크롤 이벤트가 누락되거나, 렌더 타이밍 이슈가 있을 때 useInView는 어디에서 놓친 건지 감 잡기 어렵다고 하는데 렌더 타이밍 이슈를 접하기 힘들었다.
| 상황 | 추천 트리거 | 이유 |
|---|---|---|
| 빠른 MVP, 단순 스크롤 | useInView | 설정 간단, 빠른 적용 가능 |
| 커스텀 조건, 상태 제어, 로직 확장 | useIntersectionObserver | 커스터마이징 자유도 높고, 디버깅 및 로직 분리 용이 |
| 다양한 페이지에서 동일 패턴 반복 | useIntersectionObserver | 커스텀 훅으로 추상화하여 재사용성 뛰어남 |
커스텀 훅을 기존에 만들었다면 커스텀 훅을 통해서 디테일하게 제어하는게 좋겠지만 초보자 입장에서 useInView가 간단해서 오히려 사용을 많이 할 거 같다. 라는 결론이 나왔다.
체험 관리/예약 내역 무한 스크롤 구현을 마치며 느낀 점
이번 무한 스크롤 구현은 단순한 리스트 로딩 기능 이상의 의미가 있었다. 두 개의 리스트(체험 관리, 예약 내역)를 담당하며 서로 다른 트리거 방식(useInView / 커스텀 useIntersectionObserver)을 적용해본 것은 "라이브러리 vs 직접 구현" 사이의 선택과 판단 기준을 경험하게 해주었다.
특히 useInView는 빠르게 도입할 수 있고 코드가 매우 간결해 초기 적용이 수월했지만, 트리거 제어의 유연성이 낮아 특정 상황(비동기 ref 연결, 다중 조건 분기 등)에서는 제한적이라는 단점도 드러났다.
반면 useIntersectionObserver를 커스텀 훅으로 직접 구성하면서는, 관찰 기준, 콜백 처리, 비동기 흐름 제어 등을 내가 원하는 대로 커스터마이징할 수 있었고, 그 과정에서 IntersectionObserver API 자체에 대한 이해도도 깊어졌다.
무엇보다 이번 회고에서 가장 의미 있었던 점은 "기능을 구현하는 것이 전부가 아니라, 상황에 따라 최적의 구조를 설계하고 선택하는 것이 진짜 실력"이라는 것을 체감했다는 것이다.
기술을 ‘써본다’에서 멈추지 않고, 직접 구현해보고, 비교해보고, 선택해보는 것. 이 모든 과정이 개발자로서의 시야를 확장시키는 중요한 성장의 계기였다고 느꼈다.