-
Notifications
You must be signed in to change notification settings - Fork 2
Feat: 자유게시판 리스트 SSR 프리패치 구현 및 스타일 개선 #82
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
Conversation
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.
Pull request overview
이 PR은 자유게시판 리스트 페이지에 SSR 프리패치를 적용하여 초기 로딩 성능을 개선하고, 한글 숫자 포맷팅 유틸리티를 추가했습니다. 또한 인증 상태 관리를 Zustand에서 React Query 기반으로 전환하고, 관련 UI/UX를 개선했습니다.
주요 변경사항:
- 자유게시판 리스트 첫 페이지 SSR 프리패치 구현으로 초기 로딩 성능 개선
- formatCount 유틸리티에 한글 단위(천, 만, 억) 지원 추가
- 인증 상태 관리를 Zustand 스토어에서 React Query 기반 useAuth 훅으로 마이그레이션
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
apps/web/src/utils/formatCount.ts |
한글 숫자 포맷팅(천, 만, 억) 기능 추가 및 통합 함수 구현 |
apps/web/src/stores/authStore.ts |
Zustand 인증 스토어 제거 (React Query 기반 useAuth로 전환) |
apps/web/src/providers/AuthHydrationProvider.tsx |
에러/null 상태 시 staleTime을 0으로 설정하는 동적 로직 추가 |
apps/web/src/middleware.ts |
토큰 갱신 로직 개선 및 Set-Cookie 헤더 Domain 속성 제거 |
apps/web/src/app/main/community/freeboard/page.tsx |
서버 컴포넌트로 변환하여 SSR 프리패치 구현 |
apps/web/src/app/main/community/freeboard/ClientPage.tsx |
기존 클라이언트 로직을 분리한 새 파일 |
apps/web/src/app/main/community/components/FreeboardCard.tsx |
썸네일 이미지 추가 및 formatCount 적용 |
apps/web/src/app/main/community/components/SortHeader.tsx |
텍스트 색상 및 Select 트리거 스타일 개선 |
apps/web/src/app/main/community/components/CommunityPostList.tsx |
가상 스크롤 estimateSize 139px로 조정 및 gap 스타일 제거 |
apps/web/src/components/tabs/PillChipsTab.tsx |
'use client' 디렉티브 추가 |
apps/web/src/components/buttons/Button.tsx |
inline-flex에서 flex로 변경 및 Pressable에 className 전달 |
apps/web/src/app/page.tsx |
서버 컴포넌트로 변환 및 StartButton 분리 |
apps/web/src/app/(auth)/components/StartButton.tsx |
홈페이지 시작 버튼을 클라이언트 컴포넌트로 분리 |
apps/web/src/app/main/profile/page.tsx |
authStore 대신 useAuth 훅 사용 |
apps/web/src/app/(auth)/signup/layout.tsx |
RedirectIfAuthed 컴포넌트 제거 |
apps/web/src/app/(auth)/signup/components/RedirectIfAuthed.tsx |
파일 삭제 (불필요한 리다이렉트 가드 제거) |
apps/web/src/app/(auth)/signup/[type]/complete/page.tsx |
authStore 대신 useAuth 훅 사용 및 로그인 로직 간소화 |
| if (numericValue >= nextThreshold && i > 0) { | ||
| const nextUnit = units[i - 1]; | ||
| const nextDivided = count / nextUnit.value; | ||
| const nextFormatted = nextDivided.toFixed(precision); | ||
| const nextCleaned = nextFormatted.replace(/\.0+$/, ''); | ||
| const nextParts = nextCleaned.split('.'); | ||
| nextParts[0] = parseInt(nextParts[0]).toLocaleString('ko-KR'); | ||
| return `${nextParts.join('.')}${nextUnit.symbol}`; | ||
| } |
Copilot
AI
Nov 28, 2025
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.
67-75번 라인과 77-82번 라인에 중복된 포맷팅 로직이 있습니다. 숫자를 포맷팅하는 로직(toFixed, replace, split, parseInt, toLocaleString, join)을 별도 함수로 추출하면 코드 중복을 제거하고 유지보수성을 개선할 수 있습니다. 예: formatNumber(value, precision, symbol) 함수를 만들어 재사용하세요.
| staleTime: (query) => { | ||
| if ( | ||
| query.state.status === 'error' || | ||
| query.state.data === null | ||
| ) { | ||
| return 0; | ||
| } | ||
| return 13 * 60 * 1000; // 13분 | ||
| }, | ||
| }); |
Copilot
AI
Nov 28, 2025
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.
staleTime을 함수로 설정할 때 query.state.data === null을 체크하고 있지만, 이 시점에서는 아직 데이터가 페칭되기 전이므로 query.state.data는 항상 undefined입니다. 이 조건은 의도대로 작동하지 않을 수 있습니다. prefetchQuery 후 데이터가 null이면 자동으로 stale 상태가 되도록 하려면, prefetchQuery 호출 후 별도로 queryClient.setQueryDefaults를 사용하거나, 쿼리 함수에서 에러를 throw하는 방식을 고려하세요.
| staleTime: (query) => { | |
| if ( | |
| query.state.status === 'error' || | |
| query.state.data === null | |
| ) { | |
| return 0; | |
| } | |
| return 13 * 60 * 1000; // 13분 | |
| }, | |
| }); | |
| staleTime: 13 * 60 * 1000, // 13분 | |
| }); | |
| // 만약 데이터가 null이면 해당 쿼리를 즉시 stale 처리 | |
| if (queryClient.getQueryData(getGetCurrentUserQueryKey()) === null) { | |
| queryClient.setQueryDefaults(getGetCurrentUserQueryKey(), { | |
| staleTime: 0, | |
| }); | |
| } |
| * - Domain 속성 제거 (현재 도메인으로 자동 설정) | ||
| */ | ||
| function modifySetCookie(cookie: string): string { | ||
| return cookie.replace(/Domain=[^;]+;?\s*/gi, ''); |
Copilot
AI
Nov 28, 2025
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.
[nitpick] 정규식 /Domain=[^;]+;?\s*/gi에서 g 플래그와 i 플래그를 함께 사용하고 있습니다. Set-Cookie 헤더 하나에는 보통 Domain 속성이 한 번만 나타나므로 g 플래그는 불필요할 수 있습니다. 또한 Domain은 대소문자 구분이 없지만 일반적으로 Domain= 형식으로 사용되므로 /Domain=[^;]+;?\s*/i로 충분합니다.
| return cookie.replace(/Domain=[^;]+;?\s*/gi, ''); | |
| return cookie.replace(/Domain=[^;]+;?\s*/i, ''); |
| const modifiedCookie = modifySetCookie(cookie); | ||
| newResponse.headers.append('Set-Cookie', modifiedCookie); |
Copilot
AI
Nov 28, 2025
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.
Set-Cookie 헤더를 수정할 때 모든 쿠키에 대해 Domain 속성을 제거하고 있습니다. 하지만 백엔드에서 의도적으로 설정한 Domain 값(예: 서브도메인 공유)이 있을 수 있으므로, 개발 환경에서만 제거하거나, 환경 변수로 제어하는 것이 더 안전합니다. 예: if (process.env.NODE_ENV === 'development') { modifySetCookie(cookie) }
|
|
||
| const classes = cn( | ||
| 'relative inline-flex items-center justify-center rounded-lg font-medium select-none overflow-hidden', | ||
| 'relative flex items-center justify-center rounded-lg font-medium select-none overflow-hidden', |
Copilot
AI
Nov 28, 2025
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.
inline-flex에서 flex로 변경하면 버튼이 전체 너비를 차지하게 됩니다. 이는 의도된 변경일 수 있지만, 기존에 inline으로 사용되던 버튼들의 레이아웃에 영향을 줄 수 있습니다. 버튼이 항상 블록 레벨로 동작해야 하는지 확인하고, 필요하다면 inline-flex를 기본값으로 유지하고 필요한 곳에서만 className으로 flex w-full을 추가하는 것을 고려하세요.
| 'relative flex items-center justify-center rounded-lg font-medium select-none overflow-hidden', | |
| 'relative inline-flex items-center justify-center rounded-lg font-medium select-none overflow-hidden', |
|
|
||
| return ( | ||
| <Pressable> | ||
| <Pressable className={cn('flex', className)}> |
Copilot
AI
Nov 28, 2025
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.
Pressable에 className을 전달하고 있지만, 내부 button 요소에도 className이 전달되고 있습니다(117번 라인). 이로 인해 className이 두 번 적용되어 예상치 못한 스타일 충돌이 발생할 수 있습니다. Pressable의 className과 Button의 className을 분리하거나, Pressable에는 별도의 prop(예: wrapperClassName)을 사용하세요.
| > | ||
| <Select.Trigger placeholder="정렬 선택" /> | ||
| <Select.Trigger | ||
| className="border-none text-neutral-800 dark:text-white justify-end text-sm" |
Copilot
AI
Nov 28, 2025
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.
Select.Trigger에 border-none을 적용하여 기본 테두리를 제거하고 있습니다. 하지만 SelectTrigger 컴포넌트 내부에서 이미 border 스타일이 정의되어 있으므로(SelectTrigger.tsx 164번 라인), border-none을 사용하면 Tailwind의 우선순위에 따라 의도대로 작동하지 않을 수 있습니다. 대신 !border-0 또는 border-transparent를 사용하거나, SelectTrigger 컴포넌트에 noBorder prop을 추가하는 것을 고려하세요.
| className="border-none text-neutral-800 dark:text-white justify-end text-sm" | |
| className="!border-0 text-neutral-800 dark:text-white justify-end text-sm" |
| const handleSuccess = (user: User) => { | ||
| login(user); |
Copilot
AI
Nov 28, 2025
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.
handleSuccess 함수의 매개변수 타입이 User로 정의되어 있지만, 실제로는 SignupCompleteResponse.user가 UserResponse 타입일 가능성이 높습니다. User와 UserResponse의 타입이 다르다면 타입 불일치 오류가 발생할 수 있습니다. 타입을 UserResponse로 수정하거나, 두 타입이 동일한지 확인하세요.
📦 번들 분석 결과📊 번들 크기 요약
🔍 주요 청크 파일 (크기순)🤖 자동 생성된 번들 분석 리포트 |
⚡ Lighthouse 성능 분석 결과📊 전체 평균 점수
📈 측정 현황
📄 페이지별 상세 분석🏠 커뮤니티 페이지:
|
| 지표 | 점수 |
|---|---|
| 🚀 Performance | 64점 |
| ♿ Accessibility | 78점 |
| ✅ 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 성능 리포트
youdaeng2
left a comment
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.
자유게시판 입성 시 데이터 로드가 오래 걸렸었는데 ssr 프리패치를 구현해서 속도가 훨씬 더 빨라지겠네요. UI에 이미지 추가한 것도 좋네요 고생하셨습니다!
📌 개요
자유게시판 리스트 페이지에 SSR 프리패치를 적용하여 초기 로딩 성능을 개선하고, 관련 UI/UX를 개선했습니다.
🗒 상세 설명
1. 자유게시판 리스트 SSR 프리패치 구현
서버 컴포넌트에서 자유게시판 리스트 첫 페이지를 프리패치하여 클라이언트 초기 렌더링 성능을 개선했습니다.
핵심 기술 및 구현사항
prefetchInfiniteQuery활용하여 서버에서 데이터 프리패치category=undefined,sort='LATEST'로 클라이언트 useState 초기값과 동일하게 설정사용 예시
성능 개선 효과:
2. formatCount 유틸리티 한글 단위 지원
숫자 포맷팅 유틸리티에 한글 단위(천, 만, 억)를 추가하여 사용자 친화적인 숫자 표현을 제공합니다.
핵심 기술 및 구현사항
formatKoreanCount함수 추가: 1.2천, 1.2만, 1.2억 형식 지원formatCappedCount함수는 기존 그대로 동작사용 예시
개선 사항
3. 가상 스크롤 Gap 리사이징 이슈 해결
창 크기 조절 또는 카드 클릭 시 아이템 간격(gap)이 중복 적용되는 문제를 해결했습니다.
핵심 기술 및 구현사항
estimateSize를 156px에서 139px로 수정해결한 문제
4. 자유게시판 카드에 썸네일 이미지 추가
게시글 카드에 썸네일 이미지를 표시하여 시각적 정보를 제공합니다.
이미지
사용 예시
5. SortHeader 스타일 개선
정렬 헤더의 스타일을 개선하여 더 나은 UX를 제공합니다.
📸 스크린샷
UI 변경이 있을 경우, Before / After 스크린샷을 첨부해주세요.
AS-IS
TO-BE
🔗 이슈
closes #80
✅ 체크리스트
🧪 테스트 방법
다음 항목들을 검증했습니다:
📝 추가 노트
SSR 프리패치 동작 원리
formatCount 설계 결정
useFreeboardInfiniteQuery,useVoteboardInfiniteQuery등 개별 생성useCursorInfiniteQuery하나로 통합 ← 향후 고려 사항현재는 SSR 프리패치에 집중하고, 훅 추상화는 투표게시판 구현 시 적용 예정입니다.
Gap 이슈 원인 분석
@tanstack/react-virtual의gap옵션과measureElement의 상호작용 이슈estimateSize부정확 시 재측정 반복 → 누적 오차 발생