-
Notifications
You must be signed in to change notification settings - Fork 3
feat: 대학 목록 가상화 적용 #581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 대학 목록 가상화 적용 #581
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,18 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useWindowVirtualizer } from "@tanstack/react-virtual"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import clsx from "clsx"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useRef, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ListUniversity } from "@/types/university"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import UniversityCard from "../../ui/UniverSityCard"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const UNIVERSITY_CARD_HEIGHT = 91; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const UNIVERSITY_CARD_BOTTOM_PADDING = 10; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const UNIVERSITY_CARD_GAP = 10; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ESTIMATED_UNIVERSITY_CARD_ROW_HEIGHT = UNIVERSITY_CARD_HEIGHT + UNIVERSITY_CARD_BOTTOM_PADDING; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const INITIAL_VIEWPORT_HEIGHT = 900; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| type UniversityCardsProps = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| colleges: ListUniversity[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| style?: React.CSSProperties; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -14,13 +22,81 @@ type UniversityCardsProps = { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const UniversityCards = ({ colleges, style, className, showCapacity = true, linkPrefix }: UniversityCardsProps) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const listRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [scrollMargin, setScrollMargin] = useState(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const measureScrollMargin = useCallback(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!listRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| setScrollMargin(listRef.current.getBoundingClientRect().top + window.scrollY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| measureScrollMargin(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const animationFrameId = window.requestAnimationFrame(measureScrollMargin); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener("resize", measureScrollMargin); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.cancelAnimationFrame(animationFrameId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.removeEventListener("resize", measureScrollMargin); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [measureScrollMargin]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const getItemKey = useCallback((index: number) => colleges[index]?.id ?? index, [colleges]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const virtualizer = useWindowVirtualizer({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| count: colleges.length, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| estimateSize: () => ESTIMATED_UNIVERSITY_CARD_ROW_HEIGHT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| gap: UNIVERSITY_CARD_GAP, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| getItemKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| overscan: 6, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollMargin, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| initialRect: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: INITIAL_VIEWPORT_HEIGHT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| useFlushSync: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor 🧩 Analysis chain🌐 Web query:
💡 Result: In Citations:
가상 리스트의 간격이 의도치 않게 두 배로 벌어지고 있어요 TanStack Virtual 공식 문서를 확인한 결과, 이 문제를 해결하고 시각적 일관성을 확보하기 위해 아래 변경을 제안드립니다.
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const virtualItems = virtualizer.getVirtualItems(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className={clsx("flex flex-col gap-2.5", className)} style={style}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {colleges.map((college) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div key={college.id} className="pb-2.5"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <UniversityCard university={college} showCapacity={showCapacity} linkPrefix={linkPrefix} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={listRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={clsx("relative w-full", className)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role="list" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...style, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: `${virtualizer.getTotalSize()}px`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {virtualItems.map((virtualItem) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because this component is rendered on the statically generated Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const college = colleges[virtualItem.index]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!college) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| key={virtualItem.key} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref={virtualizer.measureElement} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute left-0 top-0 w-full pb-2.5" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| data-index={virtualItem.index} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| role="listitem" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-posinset={virtualItem.index + 1} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-setsize={colleges.length} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| transform: `translateY(${virtualItem.start - virtualizer.options.scrollMargin}px)`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <UniversityCard university={college} showCapacity={showCapacity} linkPrefix={linkPrefix} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
Repository: solid-connection/solid-connect-web
Length of output: 4449
scrollMargin이 동적 높이 변화를 놓치지 않도록ResizeObserver를 함께 소개해 보아요.반갑게도 현재 코드는 창 크기 조절(
resize) 때만 고도를 재고 있어요. 하지만 검색어나 필터 칩이 줄바꿈되거나 비동기 개수 카드가 들어오면 창 크기는 그대로인데 콘텐츠 높이만 춤을 출 수 있죠. 그럴 때scrollMargin이 구린 값을 잡으면 스크롤 위치가 살짝 어긋나는 상황이 벌어질 수 있어요.따라서 아래 두 가지 걸음을 함께 걷는 건 어떨까요?
부모 영역의 호흡을 감지하는 망을 치아요
ResizeObserver를 얹어두면, 창 크기와 상관없이 높이 변화만으로도 고도 재측정을 자연스럽게 켤 수 있어요.정기적인 점검을 유지하면서도 유연하게 대응하아요
resize이벤트 리스너는 창 크기 변화에 특화된 보조 수단으로 두어,ResizeObserver가 먼저 변화에 반응하도록 해요.🤖 Prompt for AI Agents