-
Notifications
You must be signed in to change notification settings - Fork 0
커서 기반 무한스크롤 공통 훅 도입 및 안정성 개선
- PR: PR
- 적용 대상: 피드, 띱 목록, 저장된 책, 댓글 등 커서 기반 목록 화면
기존 무한스크롤은 페이지별 구현 방식이 달라(스크롤 이벤트 기반/IntersectionObserver 기반 혼재), 로딩 상태·중복 요청 방지·커서 처리 로직이 중복되면서 유지보수 비용이 증가하고 버그 재발 가능성이 있었습니다.
이를 해결하기 위해 커서 기반 무한스크롤 로직을 공통 훅 useInifinieScroll로 추상화하여, 동작 방식과 상태 관리 패턴을 통일했습니다.
- 문제 상황
- 해결 방안
- 훅 책임 범위
- 훅 구조
- 실행 흐름
- 안정성 설계 포인트(useRef 활용)
- 적용
- IntersectionObserver 선택 이유
- 기대 효과
- 성능 측정 결과
기존 무한스크롤 구현은 화면마다 방식이 달랐습니다.
- 일부 화면: window scroll 이벤트 기반
- 일부 화면: IntersectionObserver 기반
- 로딩 상태 / 중복 요청 방지 / 커서(nextCursor) / 마지막 페이지(isLast) 처리 로직이 페이지별로 중복
이로 인해
- 동일 기능 수정 시 여러 페이지를 함께 수정해야 함 (유지보수 비용 증가)
- 중복 호출, 마지막 페이지 이후 호출, 의존성 루프 등 버그가 재발할 여지 존재
커서 기반 무한스크롤을 공통 훅으로 추상화하여 구현을 통일했습니다.
- 훅 이름: useInifinieScroll (요청 네이밍 그대로 적용)
- 공통 훅이 책임지는 범위:
- 첫 페이지 로드
- 다음 페이지 로드
- items, nextCursor, isLast, 로딩 상태 관리
- IntersectionObserver 연결/해제
- 중복 실행 요청 차단(동시 요청 락)
useInifinieScroll은 “목록 화면에서 반복되는 무한스크롤 패턴”을 통째로 캡슐화합니다.
- 데이터 누적: items를 누적 관리
- 페이지 상태: nextCursor, isLast 관리
- 로딩 상태 분리: 첫 로딩(isLoading) / 추가 로딩(isLoadingMore)
- 오류 상태: error 노출
- 트리거 제공: 기반 트리거 제공
- enabled: 현재 탭/화면에서 무한스크롤 활성화 여부
- reloadKey: 값 변경 시 목록 초기화 후 첫 페이지 재조회
- fetchPage(cursor): 커서 기반 페이지 호출 함수
- mergeItems(prev, next): 병합 커스터마이징(중복 제거 등)
- rootMargin, threshold: observer 감지 조건 튜닝
- items: 누적 데이터
- nextCursor: 다음 호출 커서
- isLast: 마지막 페이지 여부
- isLoading: 첫 페이지 로딩
- isLoadingMore: 추가 로딩
- error: 에러 상태
- sentinelRef: 감시 대상 요소 ref
- isFetchingRef: 동시 요청 차단 락
- fetchPageRef: 최신 fetch 함수 참조 보장
- enabled 활성 + reloadKey 변경 감지
- 리스트/커서/마지막 페이지 상태 초기화
- fetchPage(null) 호출
- items, nextCursor, isLast 갱신
- sentinelRef가 뷰포트에 진입
- isLast, nextCursor, isFetchingRef 조건 확인
- fetchPage(nextCursor) 호출
- mergeItems로 병합 후 상태 업데이트
- 컴포넌트 업데이트/언마운트 시 observer.disconnect()로 관찰 해제
무한스크롤은 관찰 이벤트가 연속 발생할 수 있으므로, 동일 시점 중복 실행 요청 차단이 필요합니다.
- isFetchingRef.current === true이면 추가 로딩을 즉시 중단
- state가 아닌 ref를 사용하여 즉시 반영 + 불필요한 리렌더링 방지를 동시에 달성
fetchPage 함수는 컴포넌트 렌더링마다 참조가 바뀔 수 있어, 이를 그대로 의존성에 넣으면,
- useEffect가 다시 실행되며
- 불필요한 재요청/루프가 발생할 수 있습니다.
따라서 fetchPageRef.current = fetchPage로 최신 함수만 참조하도록 하여
- “최신 fetch 사용”은 보장하면서
- “렌더마다 함수 참조 변경으로 인한 재요청 루프”는 방지했습니다.
- src/hooks/useInifinieScroll.ts (공통 훅)
- src/pages/feed/Feed.tsx
- src/pages/feed/FollowerListPage.tsx
- src/pages/searchBook/SearchBook.tsx
- src/pages/notice/Notice.tsx
- src/pages/mypage/SavePage.tsx
- src/pages/memory/Memory.tsx
- src/components/group/MyGroupModal.tsx
- src/pages/groupSearch/GroupSearch.tsx
- src/components/search/GroupSearchResult.tsx
- src/components/search/GroupSearchResult.styled.ts
- src/pages/feed/FeedDetailPage.tsx
- src/components/common/Post/ReplyList.tsx
- src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx
- 무한스크롤 공통 훅 도입
- 기존 window scroll 이벤트 제거
- 탭별(피드/내 피드) 무한스크롤을 동일 패턴으로 통일
- 하단 sentinel 진입 시 다음 목록 로드
- 무한스크롤 기능 추가 (댓글 무한스크롤 로직 구현)
- src/components/common/Post/ReplyList.tsx
- src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx
useInifinieScroll를 내부에 붙여서 댓글을 직접 페이징 조회 postId/postType를 받으면 무한스크롤 모드로 동작 하단 sentinel + isLoadingMore 스피너 추가 댓글/답글 삭제 시 reload()로 목록 재조회 부모 스크롤 컨테이너 대응용 rootRef 옵션도 추가 기존 방식(commentList, onReload 주입)도 그대로 호환되게 유지
- src/pages/feed/FeedDetailPage.tsx
댓글을 부모에서 한 번에 불러오던 로직 제거 ReplyList에 postId, postType="FEED" 전달해서 내부 무한스크롤 사용 댓글 작성 성공 시 replyReloadKey 증가로 ReplyList 재조회 트리거
- 성능 효율: 스크롤 이벤트를 매 프레임 계산하지 않고 “진입 시점”에만 동작
- 중복 호출 감소: 명확한 트리거(센티넬 진입) 기반으로 불필요한 조건 체크 감소
- 구현 단순화: scrollTop/scrollHeight 계산 코드 제거 가능
- 재사용성 향상: 페이지마다 동일한 sentinel 패턴으로 통일
- 컨테이너 대응 유연성: 필요 시 root 변경으로 내부 스크롤 컨테이너도 대응 가능
- 유지보수성 향상: 관찰/해제 lifecycle이 훅 내부로 캡슐화됨
- 무한스크롤 동작의 일관성 확보
- 페이지별 중복 코드 감소 → 유지보수성 향상
- 중복 실행 요청/마지막 페이지 이후 호출 등 안정성 개선
- 신규 화면 추가 시 훅 재사용으로 개발 속도 향상
<측정 환경 및 조건>
- 도구: Chrome DevTools → Performance
- 측정 구간: 동일 사용자 시나리오(예: 피드 진입 → 스크롤로 4 페이지 추가 로드) 수행 후, 타임라인에서 해당 구간 드래그 선택
- 관측 지표
- Total time: 선택 구간 총 시간 (10초)
- Main thread time: 메인 스레드에서 실제로 바쁜 시간(체감 렉과 직결)
- Scripting time: JS 실행/계산 시간(렌더 전에 수행되는 연산 포함)
- Rendering/Painting: 렌더링/페인팅 비용

- Total: 10,000 ms
- Scripting: 3,544 ms
- Rendering: 153 ms
- Painting: 106 ms
- 스크립팅 작업 수행 시간: 3,544 / 10,000 * 100% = 약 35%

- Total: 10,000 ms
- Scripting: 2,379 ms
- Rendering: 166 ms
- Painting: 120 ms
- 스크립팅 작업 수행 시간: 2,379 / 10,000 * 100% = 약 24%
✅ 검사 결과
- Main thread time
- 큰 변화 없음
- Scripting 시간 감소율
- (3,544 - 2,379) / 3,544 * 100% = 32.87% (약 32.9% 감소)
✅ 해석
- 무한스크롤 리팩토링 이후, 스크롤 이벤트 기반 반복 계산 시간이 감소했음(스크립팅 작업 수행 시간의 비중이 감소함)을 알 수 있음
- 전체 팀 깃허브 https://github.com/THIP-TextHip
- Web 저장소 https://github.com/THIP-TextHip/THIP-Web