발행일

무한 스크롤 비교 — 쓸 것인가, 그리고 무엇으로

무한 스크롤 비교 — 쓸 것인가, 그리고 무엇으로

[TripTune](/blog/웹 기술로 만드는 협업형 여행 계획 플랫폼 TripTune 개발)의 여행지 목록과 일정 목록에 무한 스크롤을 넣기로 하면서 조사한 내용입니다.

처음엔 "어떤 라이브러리로 구현하지"부터 찾아봤는데, 그 전에 답해야 할 질문이 하나 있었습니다.

여기에 무한 스크롤이 맞나?

이 글은 그 판단과 도구 선택 기준까지입니다. 실제 구현 코드와 실무에서 물린 지점은 React Infinite Scroll 구현하기에 따로 적었습니다.


1. 무한 스크롤 vs 페이지네이션

무한 스크롤이 기본값처럼 쓰이지만, 공짜가 아닙니다.

무한 스크롤페이지네이션
탐색 흐름끊기지 않음클릭마다 끊김
"아까 그거" 다시 찾기어려움 — 몇 번째였는지 모름페이지 번호로 지목 가능
공유·북마크상태가 URL에 없음?page=3으로 그대로
뒤로가기 복원직접 구현해야 함브라우저가 해줌
총량 감각없음 — 끝을 모름"12페이지 중 3"
푸터 접근사실상 불가항상 접근
구현 난이도종료 판정·중복 방지·복원까지낮음

TripTune에서는 화면마다 다르게 갈렸습니다.

  • 여행지 탐색 목록 → 무한 스크롤. 사용자가 "둘러보는" 화면입니다. 특정 여행지를 찾으러 오는 게 아니라 스크롤하며 고르는 흐름이에요. 여기서 페이지 버튼은 흐름을 끊습니다.
  • 일정 목록 → 무한 스크롤. 내 일정은 많아야 수십 개라 애초에 끝이 가깝습니다.
  • 관리자용 표 형태 목록 → 페이지네이션. "몇 건 중 몇 건"이 필요하고 특정 행으로 다시 돌아가야 합니다.

"둘러보기"에는 무한 스크롤, "찾아가기"에는 페이지네이션. 사용자가 그 화면에서 무엇을 하려는지가 기준이었습니다. 데이터가 많고 적음이 아니었어요.

무한 스크롤을 골랐다면 같이 져야 하는 부채

  • 뒤로가기 복원. 상세로 들어갔다 돌아오면 목록 맨 위입니다. 브라우저가 해주지 않아요. 이건 스크롤 위치 기억하기에 따로 적었습니다.
  • 푸터에 못 닿음. TripTune 푸터에는 이메일과 GitHub 링크가 있는데, 목록 화면에서는 계속 밀립니다. 끝까지 스크롤하면 닿긴 하지만 그건 데이터가 끝났을 때 얘기예요.
  • 접근성. 키보드 사용자와 스크린 리더 사용자에게 "계속 늘어나는 목록"은 다루기 어렵습니다. "더 보기" 버튼을 같이 두면 해결되는데, 이 프로젝트에서는 안 했습니다.

셋 다 알고 시작한 게 아니라 겪고 나서 알았습니다. 특히 마지막은 지금도 부채로 남아 있어요.


2. 무엇으로 구현할 것인가

도구는 셋을 놓고 봤습니다.

useInfiniteQueryuseQuery + 페이지 상태useSWRInfinite
페이지 누적라이브러리가 관리 (data.pages)직접 useState에 누적라이브러리가 관리
종료 판정getNextPageParamhasNextPage직접getKeynull
로딩 상태isFetchingNextPage 제공isLoading만 (첫 로딩 전용)isValidating
페이지 번호를 밖에서 쓰기어려움쉬움 — 그냥 상태어려움
이미 프로젝트에 있나✅ TanStack Query 사용 중❌ 의존성 추가

마지막 줄이 결정적이었습니다. 이 프로젝트는 이미 TanStack Query를 쓰고 있었어요. SWR이 나쁘다는 게 아니라, 같은 일을 하는 데이터 패칭 라이브러리를 두 개 두면 캐시가 둘로 갈리고 무효화 규칙도 둘이 됩니다.

라이브러리 비교에서 "이미 프로젝트에 있는가"는 기능 비교표보다 무겁습니다. 새 도구가 20% 낫다고 두 개를 유지하는 비용을 못 이깁니다.

그리고 useSWRInfinite에 대한 정정

이 글을 처음 쓸 때 SWR의 단점으로 "react-query처럼 다음 페이지를 명확히 다루는 내장 옵션이 부족하다" 고 적었습니다. 틀렸습니다. useSWRInfinite가 정확히 그 API고, getKeynull을 반환하면 끝이라는 신호입니다.

"내가 안 찾아본 것"을 "라이브러리에 없는 것"으로 적은 것이고, 비교 글에서 제일 하지 말아야 할 일이었어요. 결론(TanStack Query 채택)은 안 바뀌지만 근거는 바뀝니다. "SWR은 기능이 부족해서"가 아니라 "이미 있는 걸 쓰려고"가 맞습니다.


3. 결국 한 가지로 통일하지 못했다

실제로는 화면마다 다르게 씁니다.

화면방식이유
일정 목록 (전체/공유/검색)useInfiniteQuery서버가 totalPages를 줌. 받은 그대로 쌓으면 됨
여행지 탐색 목록useQuery + 페이지 상태페이지 번호가 검색어·좌표 필터와 엮임
일정 편집의 여행 루트useInView + Zustand서버 페이지와 클라이언트 순서가 병합됨

세 번째는 원래 react-query로 하다가 되돌린 것이고, 그 판단이 코드에 주석으로 남아 있습니다.

// 무한 스크롤 준비
// 이전에는 react-query로 했었음
// 병합 이슈로 인하여 zustand만 살리기로!
const { ref, inView } = useInView();

"통일 못 했다"가 아니라 "통일하면 안 되는 자리가 있었다" 는 게 지금의 판단입니다. 서버가 페이지를 주는 목록과, 클라이언트에서 순서를 재조립하는 목록은 다른 문제예요.

다만 되돌린 이유를 코드에 남긴 게 핵심이었습니다. 안 남겼으면 나중에 누군가(주로 나중의 나) "여기만 왜 react-query를 안 쓰지?" 하고 다시 바꿨을 겁니다.


4. 남은 것

  • "더 보기" 버튼 대안이 없습니다. 접근성 부채(1절).
  • useSWRInfinite는 결국 안 써봤습니다. 2절의 비교는 문서를 읽은 수준이고 실측이 아닙니다.
  • 페이지네이션과의 비교에 숫자가 없습니다. 체류 시간이나 스크롤 깊이 같은 걸 재본 적이 없어요. 이 표는 일반론과 이 프로젝트에서의 체감입니다.

정리

  • 라이브러리를 고르기 전에 "이 패턴을 쓸 것인가"를 정합니다. "둘러보기"에는 무한 스크롤, "찾아가기"에는 페이지네이션.
  • 무한 스크롤을 고르면 스크롤 복원·푸터 접근·접근성이 같이 딸려옵니다. 셋 다 나중에 청구됩니다.
  • "이미 프로젝트에 있는가"는 기능 비교표보다 무겁습니다.
  • "안 찾아본 것"을 "없는 것"으로 적지 않습니다.
  • 되돌린 결정은 코드에 이유를 남깁니다. 안 남기면 원상복구됩니다.

관련 글: React Infinite Scroll 구현하기 · 무한 스크롤에서는 왜 이슈가 생긴 걸까 · React에서 스크롤 위치 기억하기