Skip to content

feat: 커뮤니티 목록 SSR 하이드레이션 적용#545

Merged
manNomi merged 1 commit into
mainfrom
feat/community-ssr-hydration
Jun 3, 2026
Merged

feat: 커뮤니티 목록 SSR 하이드레이션 적용#545
manNomi merged 1 commit into
mainfrom
feat/community-ssr-hydration

Conversation

@manNomi
Copy link
Copy Markdown
Contributor

@manNomi manNomi commented Jun 3, 2026

관련 이슈

  • 없음

작업 내용

  • 커뮤니티 게시판 목록의 초기 데이터를 서버에서 prefetch하고 HydrationBoundary로 클라이언트 React Query 캐시에 주입하도록 변경했습니다.
  • 초기 진입 및 카테고리 전환 시 빈 화면 대신 게시글 목록 형태의 스켈레톤을 노출하도록 추가했습니다.
  • 게시글 목록 query key, 초기 카테고리, 정렬 로직을 공용 유틸로 분리해 서버/클라이언트가 같은 캐시 기준을 사용하도록 정리했습니다.
  • 글 작성/수정/삭제 후 hydrated 목록 캐시가 오래 남지 않도록 postList 쿼리도 함께 무효화했습니다.
  • 첫 번째 게시글 썸네일에 eager loading 힌트를 적용해 LCP 후보 이미지 로딩을 보완했습니다.

특이 사항

  • /community/[boardCode]는 빌드 결과에서 SSR on-demand 라우트(ƒ Dynamic)로 확인됩니다.
  • 새 worktree에 의존성 링크가 없어 pnpm install --frozen-lockfile --prefer-offline로 설치만 수행했으며 lockfile 변경은 없습니다.
  • 로컬 Node 버전이 프로젝트 권장값(22.x)과 달라 Unsupported engine 경고가 출력되지만 검증은 통과했습니다.

리뷰 요구사항 (선택)

  • 서버 prefetch 데이터와 클라이언트 useGetPostList의 query key/category 기준이 일치하는지 봐주세요.
  • 글 작성/수정/삭제 이후 목록 캐시 무효화 범위가 충분한지 확인 부탁드립니다.

검증

  • pnpm --filter @solid-connect/web run lint:check
  • pnpm --filter @solid-connect/web run typecheck:ci
  • pnpm --filter @solid-connect/web run build
  • 브라우저에서 http://localhost:3000/community/FREE 렌더링 및 질문 탭 전환 확인
  • 브라우저 콘솔 에러 없음 확인

@manNomi manNomi requested review from enunsnv and wibaek as code owners June 3, 2026 03:25
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
solid-connect-university-web Error Error Jun 3, 2026 3:26am
solid-connection-web Ready Ready Preview, Comment Jun 3, 2026 3:26am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
solid-connect-web-admin Skipped Skipped Jun 3, 2026 3:26am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

Walkthrough

이 PR은 커뮤니티 게시글 목록 기능을 다층 구조로 리팩토링합니다:

  1. 쿼리 인프라 중앙화 — React Query 키를 queryKeys.ts로 분리하고, postListQuery.ts에서 게시글 정렬, 상수, 키 생성 로직을 모음
  2. 쿼리 훅 추상화getPostListQueryOptions 함수로 쿼리 설정을 캡슐화하여 서버-클라이언트 공유 가능하게 개선
  3. 뮤테이션 일관성 — 생성/수정/삭제 뮤테이션에서 무효화 대상을 통일된 CommunityQueryKeys.postList로 변경
  4. 로딩 UI 추가 — 페이지 로딩 스켈레톤 컴포넌트와 loading.tsx 라우트 추가
  5. 로딩 상태 표시 — 페이지 콘텐츠에서 isPending 플래그를 이용해 조건부 렌더링
  6. 이미지 성능 최적화 — 첫 번째 게시글 이미지에만 eager 로딩 적용
  7. 서버 프리페칭 — 페이지 로드 시 서버에서 게시글을 미리 조회하고 React Query 하이드레이션으로 클라이언트에 전달

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • wibaek
  • enunsnv
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 커뮤니티 목록 SSR 하이드레이션 적용을 명확하고 간결하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션(관련 이슈, 작업 내용, 특이 사항, 리뷰 요구사항, 검증)을 포함하고 있으며, 각 섹션이 충실하게 작성되어 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/community-ssr-hydration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/apis/community/postListQuery.ts`:
- Around line 8-9: communityPostListQueryKey currently returns ["postList",
boardCode, category] and treats "전체" and null as different keys; update
communityPostListQueryKey so it normalizes the category parameter by mapping the
string "전체" to null (e.g., const normalizedCategory = category === "전체" ? null :
category) before returning the tuple, ensuring all callers (including
prefetches) use the same cache key for the full list.

In `@apps/web/src/app/community/`[boardCode]/loading.tsx:
- Around line 5-8: TopDetailNavigation is fixed with height h-14 so
CommunityPageSkeleton can render underneath it during loading; update the
loading wrapper around TopDetailNavigation and CommunityPageSkeleton (the div
currently using className="w-full") to include a top offset equal to the nav
height (e.g., add pt-14 or equivalent padding/margin) so the skeleton content is
pushed below the fixed TopDetailNavigation; ensure the offset matches the nav's
h-14 to avoid overlap.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e3dc85e5-593a-4d22-9359-41290a2dffdc

📥 Commits

Reviewing files that changed from the base of the PR and between 0c5ce49 and 767f08b.

📒 Files selected for processing (13)
  • apps/web/src/apis/community/api.ts
  • apps/web/src/apis/community/deletePost.ts
  • apps/web/src/apis/community/getPostList.ts
  • apps/web/src/apis/community/index.ts
  • apps/web/src/apis/community/patchUpdatePost.ts
  • apps/web/src/apis/community/postCreatePost.ts
  • apps/web/src/apis/community/postListQuery.ts
  • apps/web/src/apis/community/queryKeys.ts
  • apps/web/src/app/community/[boardCode]/CommunityPageContent.tsx
  • apps/web/src/app/community/[boardCode]/CommunityPageSkeleton.tsx
  • apps/web/src/app/community/[boardCode]/PostCards.tsx
  • apps/web/src/app/community/[boardCode]/loading.tsx
  • apps/web/src/app/community/[boardCode]/page.tsx

Comment on lines +8 to +9
export const communityPostListQueryKey = (boardCode: string, category: string | null = null) =>
[CommunityQueryKeys.postList, boardCode, category] as const;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

1. 전체null을 같은 캐시 키로 정규화해 주세요.

지금은 같은 “전체 목록” 요청이 ["postList1", boardCode, null]["postList1", boardCode, "전체"]로 나뉩니다. apps/web/src/apis/community/getPostList.ts Line 16은 기본값을 null로 두고, apps/web/src/app/community/[boardCode]/page.tsx Lines 44-48은 "전체"로 프리패치하고 있어서, 다른 호출부가 기본값을 쓰면 하이드레이션된 캐시를 재사용하지 못하고 한 번 더 요청하게 됩니다.

🔧 제안 코드
 export const COMMUNITY_INITIAL_CATEGORY = "전체";
 export const COMMUNITY_POST_LIST_STALE_TIME = Infinity;
 export const COMMUNITY_POST_LIST_GC_TIME = 1000 * 60 * 30; // 30분

+export const normalizeCommunityPostListCategory = (category: string | null = null) =>
+  !category || category === COMMUNITY_INITIAL_CATEGORY ? null : category;
+
 export const communityPostListQueryKey = (boardCode: string, category: string | null = null) =>
-  [CommunityQueryKeys.postList, boardCode, category] as const;
+  [CommunityQueryKeys.postList, boardCode, normalizeCommunityPostListCategory(category)] as const;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const communityPostListQueryKey = (boardCode: string, category: string | null = null) =>
[CommunityQueryKeys.postList, boardCode, category] as const;
export const COMMUNITY_INITIAL_CATEGORY = "전체";
export const COMMUNITY_POST_LIST_STALE_TIME = Infinity;
export const COMMUNITY_POST_LIST_GC_TIME = 1000 * 60 * 30; // 30분
export const normalizeCommunityPostListCategory = (category: string | null = null) =>
!category || category === COMMUNITY_INITIAL_CATEGORY ? null : category;
export const communityPostListQueryKey = (boardCode: string, category: string | null = null) =>
[CommunityQueryKeys.postList, boardCode, normalizeCommunityPostListCategory(category)] as const;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/apis/community/postListQuery.ts` around lines 8 - 9,
communityPostListQueryKey currently returns ["postList", boardCode, category]
and treats "전체" and null as different keys; update communityPostListQueryKey so
it normalizes the category parameter by mapping the string "전체" to null (e.g.,
const normalizedCategory = category === "전체" ? null : category) before returning
the tuple, ensuring all callers (including prefetches) use the same cache key
for the full list.

Comment on lines +5 to +8
<div className="w-full">
<TopDetailNavigation title="커뮤니티" />
<CommunityPageSkeleton />
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

1) 로딩 상태에서 상단 고정 네비게이션과 본문이 겹칠 수 있어요.

`TopDetailNavigation`가 `fixed` 높이(`h-14`)라서, 현재 구조에서는 스켈레톤 시작 영역이 네비 뒤로 들어갈 수 있습니다. 로딩 화면 래퍼에 상단 오프셋을 맞춰 주세요.
수정 예시
 const CommunityLoading = () => (
-  <div className="w-full">
+  <div className="w-full pt-14">
     <TopDetailNavigation title="커뮤니티" />
     <CommunityPageSkeleton />
   </div>
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="w-full">
<TopDetailNavigation title="커뮤니티" />
<CommunityPageSkeleton />
</div>
<div className="w-full pt-14">
<TopDetailNavigation title="커뮤니티" />
<CommunityPageSkeleton />
</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/community/`[boardCode]/loading.tsx around lines 5 - 8,
TopDetailNavigation is fixed with height h-14 so CommunityPageSkeleton can
render underneath it during loading; update the loading wrapper around
TopDetailNavigation and CommunityPageSkeleton (the div currently using
className="w-full") to include a top offset equal to the nav height (e.g., add
pt-14 or equivalent padding/margin) so the skeleton content is pushed below the
fixed TopDetailNavigation; ensure the offset matches the nav's h-14 to avoid
overlap.

@manNomi manNomi merged commit d0779ed into main Jun 3, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant