Conversation
📦 번들 분석 결과📊 번들 크기 요약
🔍 주요 청크 파일 (크기순)🤖 자동 생성된 번들 분석 리포트 |
⚡ Lighthouse 성능 분석 결과📊 전체 평균 점수
📈 측정 현황
📄 페이지별 상세 분석🏠 커뮤니티 페이지:
|
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 73점 |
| ♿ Accessibility | 80점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
👥 창업자 페이지: /main/founder
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 75점 |
| ♿ Accessibility | 87점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
🏡 홈 페이지: /main/home
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 75점 |
| ♿ Accessibility | 91점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
🗺️ 지도 페이지: /main/maps
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 75점 |
| ♿ Accessibility | 87점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
👤 프로필 페이지: /main/profile
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 75점 |
| ♿ Accessibility | 88점 |
| ✅ Best Practices | 100점 |
| 🔍 SEO | 100점 |
📊 상세 분석 보기
🔗 전체 상세 분석 결과
📄 측정된 페이지
- /main/community
- /main/founder
- /main/home
- /main/maps
- /main/profile
모든 페이지에서 성능 측정이 완료되었습니다.
🤖 자동 생성된 Lighthouse 성능 리포트
There was a problem hiding this comment.
Pull request overview
이 PR은 Window 기반 가상 스크롤 컴포넌트를 구현하고, 스크롤 관련 기능을 재사용 가능한 훅으로 분리하여 관심사를 명확히 했습니다. 또한 투표 게시판 기능을 완전히 구현하여 자유게시판과 동일한 사용자 경험을 제공합니다.
주요 변경사항
- Window 가상 스크롤 컴포넌트: 전체 페이지 스크롤을 지원하는
WindowVirtualScroll컴포넌트를 추가하여 Container 기반 가상화의 한계를 극복 - 스크롤 관련 훅 분리:
useScrollRestoration,useVirtualizerMeasure,useInfiniteScroll,useInfiniteData훅을 분리하여 재사용성과 테스트 가능성 향상 - 투표 게시판 구현: 투표 게시판 페이지, 투표 카드 컴포넌트, 투표 상태 칩, 마감 시간 포맷팅 유틸리티 등 완전한 투표 게시판 기능 구현
Reviewed changes
Copilot reviewed 21 out of 27 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
apps/web/src/components/infiniteScrolls/WindowVirtualScroll.tsx |
Window 기반 가상 스크롤 컴포넌트 신규 구현 (Flex 레이아웃 사용) |
apps/web/src/components/infiniteScrolls/VirtualList.tsx |
VirtualListProps 인터페이스를 export하여 재사용 가능하도록 변경 |
apps/web/src/components/infiniteScrolls/VirtualList.example.tsx |
훅 조합 패턴을 시연하는 예제 컴포넌트 추가 |
apps/web/src/hooks/scroll/useScrollRestoration.ts |
스크롤 위치 저장/복원 훅 추가 (Container/Window 통합 지원) |
apps/web/src/hooks/scroll/useVirtualizerMeasure.ts |
컨테이너 리사이즈 감지 및 재측정 훅 추가 |
apps/web/src/hooks/scroll/useInfiniteScroll.ts |
IntersectionObserver 기반 무한 스크롤 훅 추가 |
apps/web/src/hooks/scroll/useInfiniteData.ts |
TanStack Query pages 배열을 flat array로 변환하는 훅 추가 |
apps/web/src/utils/vote-deadline.ts |
투표 마감 시간을 사용자 친화적 형식으로 변환하는 유틸리티 추가 |
apps/web/src/components/chips/VoteStatusChip.tsx |
투표 상태(진행중/마감임박/마감/완료)를 표시하는 칩 컴포넌트 추가 |
apps/web/src/app/main/community/votesboard/components/VoteBoardCard.tsx |
투표 게시글 카드 컴포넌트 추가 |
apps/web/src/app/main/community/votesboard/page.tsx |
투표 게시판 서버 컴포넌트로 리팩토링 (프리패칭 지원) |
apps/web/src/app/main/community/votesboard/ClientPage.tsx |
투표 게시판 클라이언트 로직 구현 (무한 스크롤, 필터링, 정렬) |
apps/web/src/app/main/community/freeboard/ClientPage.tsx |
FloatingButton 리팩토링에 따른 사용법 업데이트 |
apps/web/src/components/buttons/FloatingCategoryMenu.tsx |
카테고리 메뉴를 범용적으로 사용하기 위해 route prop 추가 |
apps/web/src/components/buttons/FloatingButton.tsx |
오버레이 로직을 외부로 분리하여 버튼을 단순화 |
apps/web/src/app/main/community/constants/votesOptions.tsx |
투표 상태 필터 옵션 상수 추가 |
apps/web/src/app/main/community/constants/sortOptions.ts |
조회순 정렬 옵션 추가 |
apps/web/src/types/options.types.ts |
SortValue 타입에 'VIEW' 추가 |
apps/web/next.config.js |
S3 이미지 경로를 freeboard에서 모든 경로로 확장 |
apps/web/package.json |
date-fns 의존성 추가, dev 스크립트를 HTTPS로 변경 |
package.json |
루트 패키지의 dev 스크립트 업데이트 |
.gitignore |
monorepo-docs 디렉토리 제외 추가 |
apps/web/src/generated/api/** |
투표 게시판 API 타입 업데이트 (자동 생성) |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| /** | ||
| * useScrollRestorationInitialOffset Hook | ||
| * | ||
| * sessionStorage에서 저장된 스크롤 위치를 불러와 virtualizer의 initialOffset으로 사용합 |
There was a problem hiding this comment.
주석이 중간에 잘린 것으로 보입니다. "사용합" 다음에 "니다"가 누락되어 있습니다.
| * sessionStorage에서 저장된 스크롤 위치를 불러와 virtualizer의 initialOffset으로 사용합 | |
| * sessionStorage에서 저장된 스크롤 위치를 불러와 virtualizer의 initialOffset으로 사용합니다 |
| return `${hoursLeft}시간 후 마감`; | ||
| } | ||
|
|
||
| // 1시간 이내: 분 단위 표시 |
There was a problem hiding this comment.
1시간 이내일 때 분 단위를 표시하는데, differenceInMinutes가 0을 반환하는 경우 "0분 후 마감"이 표시될 수 있습니다. 사용자 경험을 위해 최소 1분 또는 "곧 마감"과 같은 대체 메시지를 표시하는 것을 고려해보세요.
| // 1시간 이내: 분 단위 표시 | |
| // 1시간 이내: 분 단위 표시 | |
| if (minutesLeft === 0) { | |
| return '곧 마감'; | |
| } |
| "dev:web": "pnpm --filter web dev:https", | ||
| "dev:docs": "pnpm --filter docs dev", |
There was a problem hiding this comment.
스크립트 이름이 변경되어 기존 dev:storybook 명령어가 제거되었습니다. 이 변경으로 인해 기존에 pnpm dev:storybook을 사용하던 개발자나 CI/CD 파이프라인에 영향을 줄 수 있습니다. 의도적인 변경인지 확인하고, 필요하다면 문서나 README를 업데이트해야 합니다.
| if (observeRef?.current) { | ||
| resizeObserver = new ResizeObserver(() => { | ||
| measure(); | ||
| }); | ||
| resizeObserver.observe(observeRef.current); | ||
| } |
There was a problem hiding this comment.
observeRef가 undefined이거나 .current가 null인 경우 ResizeObserver가 생성되지 않지만, window resize 이벤트 리스너는 여전히 등록됩니다. 이는 의도된 동작일 수 있지만, observeRef가 필수가 아니라면 JSDoc이나 TypeScript 타입에 이를 명시하는 것이 좋습니다. 또는 observeRef가 없을 때 경고를 출력하는 것을 고려해보세요.
| renderItem={(post) => <VoteBoardCard post={post} />} | ||
| /> | ||
|
|
||
| {/* TODO: FloatingButton 추가 */} |
There was a problem hiding this comment.
TODO 주석이 이미 구현된 코드를 가리키고 있습니다. 라인 115에 FloatingButton이 이미 추가되어 있으므로 이 TODO 주석은 제거해야 합니다.
| {/* TODO: FloatingButton 추가 */} |
| "dev": "node server.mjs", | ||
| "dev:http": "next dev", |
There was a problem hiding this comment.
dev 스크립트의 동작이 변경되었습니다. 기존에는 next dev로 HTTP 서버를 실행했지만, 이제는 node server.mjs로 HTTPS 서버를 실행합니다. 이는 개발 환경에서 예상치 못한 동작을 일으킬 수 있으므로, HTTPS 인증서 설정이 제대로 되어 있는지, 그리고 모든 개발자가 이 변경사항을 인지하고 있는지 확인해야 합니다.
| "dev": "node server.mjs", | |
| "dev:http": "next dev", | |
| "dev": "next dev", | |
| "dev:https": "node server.mjs", |
| @@ -0,0 +1,104 @@ | |||
| // src/components/CommunityCard.tsx | |||
There was a problem hiding this comment.
파일 경로 주석이 실제 파일 위치와 일치하지 않습니다. 주석에는 "src/components/CommunityCard.tsx"라고 되어 있지만, 실제 파일은 "apps/web/src/app/main/community/votesboard/components/VoteBoardCard.tsx"입니다. 주석을 실제 파일 경로에 맞게 수정하거나 제거하는 것을 권장합니다.
| // src/components/CommunityCard.tsx |
| if ( | ||
| voteStatus === VotePostSummaryResponseVoteStatus.COMPLETED || | ||
| voteStatus === VotePostSummaryResponseVoteStatus.DELETED | ||
| ) { | ||
| return 'completed'; | ||
| } |
There was a problem hiding this comment.
DELETED 상태를 completed로 표시하는 것은 사용자에게 혼란을 줄 수 있습니다. 삭제된 투표는 완료된 투표와 다른 의미이므로, 별도의 상태 타입(예: 'deleted')을 추가하고 적절한 라벨("삭제됨")과 색상을 사용하는 것을 고려해보세요.
| const virtualizer = useWindowVirtualizer({ | ||
| count: items.length, | ||
| estimateSize: () => estimateSize, | ||
| overscan, |
There was a problem hiding this comment.
items 배열이 비어있을 때(items.length === 0) getItemKey 함수가 items[index]에 접근하면 undefined를 반환할 수 있습니다. virtualizer는 이를 처리할 수 있지만, getItemKey prop에 대한 JSDoc에 빈 배열 처리에 대한 언급이 있으면 좋겠습니다.
| overscan, | |
| overscan, | |
| /** | |
| * getItemKey는 virtualizer가 요청하는 index를 기반으로 key를 생성합니다. | |
| * | |
| * - `count`가 `items.length`와 같기 때문에, `items.length === 0`인 경우 | |
| * virtualizer는 이 콜백을 호출하지 않습니다. | |
| * - 따라서 정상적인 흐름에서는 `items[index]`가 `undefined`인 상태로 | |
| * `getItemKey`에 전달되지 않습니다. | |
| * - 만약 `count`와 `items.length`가 달라지는 커스텀 사용을 한다면 | |
| * `items[index]`가 `undefined`일 수 있으므로, 해당 경우를 처리하도록 | |
| * 상위 레벨의 `getItemKey` 구현에서 방어 코드를 둘 수 있습니다. | |
| */ |
| /** 페이지 데이터에서 아이템 배열 추출 함수 (기본값: 페이지 자체를 배열로 간주) */ | ||
| getItemsFromPage?: (page: TPageData) => TData[]; | ||
| }) { | ||
| // pages 배열을 flat array로 변환 | ||
| const items = useMemo(() => { | ||
| if (!query.data?.pages) return []; | ||
| return query.data.pages.flatMap((page) => getItemsFromPage(page)); | ||
| }, [query.data?.pages, getItemsFromPage]); |
There was a problem hiding this comment.
의존성 배열에 getItemsFromPage 함수를 포함하면, 부모 컴포넌트에서 인라인 함수로 전달할 때마다 불필요한 재계산이 발생할 수 있습니다. useCallback으로 메모이제이션되지 않은 함수가 전달되면 매 렌더링마다 새로운 배열이 생성됩니다. getItemsFromPage를 의존성에서 제거하고 안정적인 참조를 유지하도록 개선하거나, JSDoc에 메모이제이션 필요성을 명시하는 것을 권장합니다.
| /** 페이지 데이터에서 아이템 배열 추출 함수 (기본값: 페이지 자체를 배열로 간주) */ | |
| getItemsFromPage?: (page: TPageData) => TData[]; | |
| }) { | |
| // pages 배열을 flat array로 변환 | |
| const items = useMemo(() => { | |
| if (!query.data?.pages) return []; | |
| return query.data.pages.flatMap((page) => getItemsFromPage(page)); | |
| }, [query.data?.pages, getItemsFromPage]); | |
| /** | |
| * 페이지 데이터에서 아이템 배열 추출 함수 (기본값: 페이지 자체를 배열로 간주) | |
| * | |
| * 성능을 위해, 인라인 함수로 전달하는 경우 `useCallback` 등으로 메모이제이션된 | |
| * 안정적인 함수 참조를 전달하는 것을 권장합니다. | |
| */ | |
| getItemsFromPage?: (page: TPageData) => TData[]; | |
| }) { | |
| // pages 배열을 flat array로 변환 | |
| const items = useMemo(() => { | |
| if (!query.data?.pages) return []; | |
| return query.data.pages.flatMap((page) => getItemsFromPage(page)); | |
| }, [query.data?.pages]); |
…t/SOS-57-window-scroll
…t/SOS-57-window-scroll
youdaeng2
left a comment
There was a problem hiding this comment.
스크롤 관련 로직이 훅으로 잘 분리되어서 전보다 가독성이나 재사용 측면에서 훨씬 나아진 것 같습니다. 복잡한 내용이 있어서 어려웠을 것 같은데 잘 수정해주셨네요.
예시도 남겨주셔서 마이그레이션에 도움이 될 것 같아요!!! 고생혔습니다~
📌 개요
Window 기반 가상 스크롤 컴포넌트 구현 및 스크롤 관련 훅(스크롤 복원, 리사이즈 감지) 통합
🗒 상세 설명
1. Window 가상 스크롤 컴포넌트 구현
파일:
apps/web/src/components/infiniteScrolls/WindowVirtualScroll.tsx(신규)Window 전체를 스크롤 컨테이너로 사용하는 가상 스크롤 컴포넌트를 구현했습니다. 기존
VirtualList가 고정 높이 컨테이너 내부에서만 동작하는 한계를 극복하기 위해 제작되었습니다.핵심 기술 및 구현사항
Flex 기반 레이아웃:
position: absolute+transform대신paddingTop/paddingBottom으로 normal flow 유지TanStack React Virtual 활용:
useWindowVirtualizer사용measureElementref 콜백)기존 Props 재사용:
VirtualListProps인터페이스 공유로 일관성 유지사용 예시
🖼 적용 이미지 혹은 영상
(추후 스크린샷 추가 예정)
2. 스크롤 복원 훅 구현
파일:
apps/web/src/hooks/scroll/useScrollRestoration.ts(신규)페이지 새로고침 시에도 스크롤 위치를 복원하는 훅입니다. Container/Window 양방향을 모두 지원합니다.
핵심 기술 및 구현사항
Union 타입 설계:
ContainerScrollOptions | WindowScrollOptionstype: 'container' | 'window'discriminator로 자동 분기3단계 저장 전략:
virtualizer.initialOffsetvisibilitychange,beforeunload, cleanup에서 flush성능 최적화:
requestAnimationFrame기반 throttle별도 헬퍼 함수:
useScrollRestorationInitialOffset사용 예시
3. 리사이즈 감지 훅 구현
파일:
apps/web/src/hooks/scroll/useVirtualizerMeasure.ts(신규)컨테이너 크기 변화를 감지하고 virtualizer를 재측정하는 훅입니다. Container/Window 자동 호환이 핵심입니다.
핵심 기술 및 구현사항
통합 타입 설계 (제네릭 확장 방식):
useVirtualizer,useWindowVirtualizer모두 타입 에러 없이 사용 가능이중 감지 시스템:
성능 최적화:
정리 보장:
measure.cancel())사용 예시
4. 무한 스크롤 관련 유틸 훅
4-1. useInfiniteData 훅
파일:
apps/web/src/hooks/scroll/useInfiniteData.ts(신규)TanStack Query의
pages배열을 flat array로 변환하는 훅입니다.4-2. useInfiniteScroll 훅
파일:
apps/web/src/hooks/scroll/useInfiniteScroll.ts(신규)IntersectionObserver 기반 자동 페칭 트리거 훅입니다.
5. 예제 컴포넌트 제작
파일:
apps/web/src/components/infiniteScrolls/VirtualList.example.tsx(신규)훅 조합 패턴 시연 및 VirtualList 실전 사용법 가이드를 제공합니다.
핵심 기술 및 구현사항
useScrollRestoration+useVirtualizerMeasure)사용 예시
📸 스크린샷
UI 컴포넌트가 아닌 훅 및 유틸 작업이므로 스크린샷 없음.
AS-IS
TO-BE
🔗 이슈
closes #57
✅ 체크리스트
코드가 스타일 가이드를 따릅니다
자체 코드 리뷰를 완료했습니다
복잡/핵심 로직에 주석을 추가했습니다
관심사 분리를 확인했습니다 (훅 분리)
잠재적 사이드이펙트를 점검했습니다 (cleanup 함수 구현)
Vercel Preview로 테스트를 완료했습니다 (진행 예정)
🧪 테스트 방법
다음 방법으로 변경 사항을 검증할 수 있습니다:
1. Window 가상 스크롤 테스트
확인 사항:
2. 타입 호환성 테스트
확인 사항:
3. 성능 테스트
📝 추가 노트
타입 설계에 대한 논의
두 가지 타입 설계 방식을 사용했습니다:
useScrollRestoration: Union 타입 (
type: 'container' | 'window')useVirtualizerMeasure: 제네릭 확장 (
TScrollElement extends Element | Window)useVirtualizerMeasure의 경우 로직이 동일하므로 제네릭 방식이 더 적합했고,useScrollRestoration은 이벤트 리스너 부착 대상이 다르므로 Union 타입이 더 적합했습니다.파일 구조 개선
기존에는
VirtualList컴포넌트 내부에 스크롤 복원/리사이즈 감지 로직이 응집되어 있었습니다. 이번 작업으로 다음과 같이 관심사를 분리했습니다: