Skip to content

Conversation

@SingTheCode
Copy link
Contributor

@SingTheCode SingTheCode commented Oct 25, 2025

🎯 목적

기존의 "Load More" 버튼을 제거하고 무한 스크롤(Infinite Scroll) 기능을 구현하여 사용자 경험을 개선합니다. 사용자가 스크롤을 내리면 자동으로 다음 페이지의 커밋 데이터를 로드하여 끊김 없는 탐색 환경을 제공합니다.

📋 주요 변경사항

1. 커스텀 훅 추가

  • useInfiniteScroll 훅 구현 (packages/view/src/hooks/useInfiniteScroll.ts)
    • IntersectionObserver API를 활용한 재사용 가능한 무한 스크롤 훅
    • 설정 가능한 threshold 및 rootMargin 옵션 제공
    • enabled 플래그로 활성화/비활성화 제어

2. Summary 컴포넌트 개선

  • Infinite Scroll 통합 (packages/view/src/components/VerticalClusterList/Summary/Summary.tsx)

    • Sentinel element 방식으로 IntersectionObserver 구현
    • 리스트 하단에 감시 요소를 배치하여 스크롤 감지
    • onLoadMore, isLoadingMore, isLastPage, enabled props 추가
    • 오버플로우 스타일 조정 (부모 컨테이너에서 스크롤 처리)
  • Author 이미지 에러 핸들링 개선 (packages/view/src/components/Common/Author/Author.tsx)

    • 이미지 로드 실패 시 사용자 이름 첫 글자로 대체
    • AuthorInfo 타입에서 src 속성을 optional로 변경
    • isGitHubUser 체크 로직에서 undefined 값 처리

3. VerticalClusterList 연동

  • 무한 스크롤 로직 연결 (packages/view/src/components/VerticalClusterList/VerticalClusterList.tsx)
    • handleLoadMore 콜백 추가 (다음 페이지 커밋 fetch)
    • isLoadingMore 상태 관리로 중복 요청 방지
    • Summary 컴포넌트에 무한 스크롤 제어를 위한 props 전달
    • 무한 스크롤 트리거 엘리먼트 스타일 추가

4. App 컴포넌트 리팩토링

  • Load More 버튼 제거 (packages/view/src/App.tsx)
    • 수동 Load More 버튼 UI 및 관련 로직 완전 제거
    • handleLoadMore 함수 및 관련 상태 제거
    • 초기 로딩 시에만 BounceLoader 표시하도록 단순화
    • 불필요한 imports 및 상태 구독 정리

5. 데이터 검증 강화

  • useAnalayzedData 훅 개선 (packages/view/src/hooks/useAnalayzedData.ts)
    • clusterNodes가 유효한 배열인지 검증
    • 잘못된 페이지네이션 응답으로 인한 크래시 방지
    • 에러 로깅 및 로딩 상태 처리

🔗 관련 이슈

Closes #1003

공유할 내용

다음 클러스터 목록을 빌드하는 동안 로딩을 표시할 스피너 표시는 해당 PR에서 구현하지 않았습니다. 다른 PR 에서 구현할 생각인데 최종 보고서 전에는 불가할 것 같습니다.

SingTheCode and others added 6 commits October 25, 2025 22:17
Add a custom hook that wraps IntersectionObserver API for implementing
infinite scroll functionality. The hook provides a ref to attach to a
sentinel element and triggers a callback when the element becomes visible.

Features:
- Configurable threshold and rootMargin
- Automatic cleanup on unmount
- Can be enabled/disabled via enabled prop

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Implement infinite scroll using IntersectionObserver with sentinel element approach:
- Add sentinel element at the end of the list to detect when user scrolls to bottom
- Set up IntersectionObserver to trigger onLoadMore callback when sentinel is visible
- Update props to accept onLoadMore, isLoadingMore, isLastPage, and enabled
- Modify overflow style to hidden (parent will handle scrolling)
- Improve author image loading with better error handling and fallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Wire up infinite scroll functionality in VerticalClusterList:
- Add handleLoadMore callback that fetches next page of commits
- Track loading state with isLoadingMore to prevent duplicate requests
- Pass props to Summary component for infinite scroll control
- Add styles for infinite scroll trigger element

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove the manual Load More button UI as it's been replaced with infinite scroll:
- Remove handleLoadMore function and related state
- Remove load-more-container and load-more-button styles
- Simplify loading state check to only show BounceLoader on initial load
- Remove unused imports and state subscriptions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Enhance author avatar handling to gracefully handle missing or failed images:
- Update AuthorInfo type to allow undefined src
- Add fallback avatar showing first letter of name when src is undefined
- Improve isGitHubUser check to handle undefined values
- Update AuthorBarChart to handle undefined profile images

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add validation check to ensure clusterNodes is a valid array before processing:
- Check if clusterNodes is an array before using it
- Log error and stop loading if invalid data is received
- Prevents crashes from malformed pagination responses

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
SingTheCode and others added 3 commits October 25, 2025 23:06
Streamlined code comments in the Summary component by removing redundant explanations and translating Korean comments to English for better international collaboration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Create IntersectionObserver only once and reuse it for performance.
- Add `isObservingRef` to prevent redundant observer operations.
- Improve observer lifecycle management for infinite scrolling.
Updated COMMIT_COUNT_PER_PAGE constant to load more commits per page, improving user experience by reducing the number of load operations needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
yuminnnnni
yuminnnnni previously approved these changes Oct 28, 2025
Copy link
Member

@yuminnnnni yuminnnnni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무너무 수고하셨습니다!! 👍👍

if (!isLastPage && stopIndex >= clusters.length && sentinelRef.current && enabled) {
// Clean up existing observer if present
if (observerRef.current) {
observerRef.current.disconnect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 observer를 매번 지우는 이유가 따로 있을까요?? IntersectionObserver의 observe()로 매번 자동 감시가 되는 걸로 알고 있어서요!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 습관(??) 이 아닐까요? 😄

Copy link
Contributor Author

@SingTheCode SingTheCode Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuminnnnni
의견 너무 감사합니다ㅎㅎ
isLoadingMore ,enabled 로 다음 페이지 호출 여부를 컨트롤하게 되는데, enabled, isLoadingMore가 변경될 때 IntersectionObserver 콜백함수가 생성시점의 값을 클로저로 가지고 있어서 변경된 상태값이 반영되지 않습니다.

그래서 변경될 때마다 Observer를 다시 등록해주도록 구현하였습니다ㅎㅎ

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금보니 useRef로 enabled, isLoadingMore를 컨트롤 해주었으면 어떨까싶네요ㅎㅎ 추후 작업때 반영해보겠습니다ㅎㅎ

ytaek
ytaek previously approved these changes Oct 30, 2025
Copy link
Contributor

@ytaek ytaek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그냥 lib 붙이면 되는게 아니라 신경쓸게 꽤 많았군요!!
고생하셨습니다!!!! LGreatTM


useEffect(() => {
if (initRef.current === false) {
if (!initRef.current) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

const isGitHubUser = (src: string): boolean => {
return src.startsWith(GITHUB_URL);
const isGitHubUser = (src: string | undefined): boolean => {
return Boolean(src?.startsWith(GITHUB_URL));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 여쭤보려다가 copilot 한테 물어봤습니다.

'''
GitHub Copilot Chat Assistant

간단히: src가 undefined일 수 있어서요. src?.startsWith(...)는 src가 없으면 undefined를 반환하고, 함수는 항상 boolean을 반환해야 하므로 Boolean(...)으로 감싸서 undefined를 false로 바꿔 일관된 boolean을 반환하게 한 것입니다.

세부:

src?.startsWith(GITHUB_URL) 결과는 true | false | undefined 입니다 (src가 없으면 undefined).
Boolean(undefined) → false 이므로 함수 시그니처(boolean)를 충족하고 호출부에서 안전하게 사용 가능합니다.
대체로 쓸 수 있는 깔끔한 패턴들:

return src?.startsWith(GITHUB_URL) ?? false; // optional chaining + nullish coalescing
return !!src && src.startsWith(GITHUB_URL); // !!로 boolean 캐스트
return (src ?? "").startsWith(GITHUB_URL); // 빈 문자열로 대체
'''

copilot 잘하네요 ㅋㅋ

};

const AvatarComponent = ({ name, src }: AuthorInfo) => {
// src가 undefined인 경우 이름의 첫 글자를 표시하는 기본 아바타 사용
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석은 추후에 삭제해도 괜찮을 것 같습니다. 코드만 봐도 직관적으로 알 수있게 잘 짜져있네요 😈

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 삭제했습니다! 확인 감사합니다ㅎㅎ

if (!isLastPage && stopIndex >= clusters.length && sentinelRef.current && enabled) {
// Clean up existing observer if present
if (observerRef.current) {
observerRef.current.disconnect();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 습관(??) 이 아닐까요? 😄


const getRowHeight = ({ index }: { index: number }) => {
if (!isLastPage && index === clusters.length) {
return 10;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 상수로 빼시죵!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다! 의견 감사합니다ㅎㅎ

- Extract repeated sentinel row condition into isSentinelRow helper function in Summary component
- Remove Korean comment from Author component for code consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@SingTheCode SingTheCode dismissed stale reviews from ytaek and yuminnnnni via 3cd56a0 October 31, 2025 14:44
@ytaek ytaek merged commit 03e41e1 into main Oct 31, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[new feature]: 클러스터 목록 무한 스크롤 기능 구현

4 participants