Skip to content

[refactor] 모임 상세 페이지 성능 최적화 - #230

Open
eojindesu wants to merge 7 commits into
devfrom
refactor/#222/moim-detail-optimization
Open

[refactor] 모임 상세 페이지 성능 최적화#230
eojindesu wants to merge 7 commits into
devfrom
refactor/#222/moim-detail-optimization

Conversation

@eojindesu

@eojindesu eojindesu commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

📌 Summary

모임 상세 페이지 Lighthouse 성능 측정 후, 최적화를 진행했습니다.

📄 Tasks

1. 이미지 최적화

  • 히어로 이미지 영역을 별도 컴포넌트로 분리
  • 히어로 이미지에 priority, sizes 속성 적용
  • 추천 모임 카드 이미지에 sizes 속성 추가

LCP 이미지에 우선순위를 부여하고 화면 크기에 맞는 이미지가 로드되도록 최적화하였습니다.

2. 추천 모임 섹션 구조 개선

  • 추천 모임 영역을 초기 렌더링에서 분리
  • Suspense + lazy 렌더링 적용
  • 제목은 즉시 렌더링, 카드 영역은 이후 로딩되도록 구조 개선
  • RecommendedMeetingsClient를 dynamic import로 분리하여 client-side에서만 렌더링되도록 적용
  • 초기 번들에서 추천 모임 관련 JS 제거 → hydration 비용 감소

3. 렌더링 최적화

  • MoimDetailClient의 불필요한 RESET useEffect 제거
  • InformationContainer에서 반복적으로 생성되던 값 및 핸들러를 memoization 처리
  • 불필요한 re-render 방지

4. 추가 리팩토링

  • 추천 로직 개선
    • 기존에는 마감된 모임도 우선적으로 노출되는 문제가 있었습니다.
    • 마감되지 않은 모임을 우선적으로 추천하도록 정렬 로직을 수정하였습니다.
  • 모임 상세 페이지 진입 시 스크롤 위치가 상단으로 초기화되도록 하는 로직 훅으로 분리 (_pages/moim-detail/lib/scroll-reset.tsx)

5. 성능 개선 결과

  • LCP 개선 (2.6s → 2.5s)
  • 전체 Performance Score 상승 (85 → 87)

👀 To Reviewer

  • Lighthouse 측정 결과는 하단에 스크린샷으로 첨부해두었습니다.
  • 점수의 큰 변화는 없어서 ^..^ 추가적인 성능 개선을 진행할 예정입니다.

📸 Screenshot (Before / After 비교)

빌드 환경 + 시크릿 모드 + Lighthouse 점수 Before

스크린샷 2026-04-15 오후 6 10 41 스크린샷 2026-04-15 오후 6 11 24

빌드 환경 + 시크릿 모드 + Lighthouse 점수 After

스크린샷 2026-04-15 오후 5 13 23 스크린샷 2026-04-15 오후 5 13 37

Summary by CodeRabbit

릴리즈 노트

  • 새로운 기능

    • 모임 상세 페이지에 히어로 이미지 영역 추가
    • 추천 모임 섹션 추가 및 좋아요 기능 제공
    • 페이지 네비게이션 시 자동 스크롤 상단 이동 기능 추가
  • 개선사항

    • 추천 모임 정렬 로직 개선 - 모집 중인 모임을 우선 표시
    • 페이지 성능 최적화로 더욱 빠른 로딩 속도 제공

@eojindesu eojindesu self-assigned this Apr 15, 2026
@eojindesu
eojindesu requested a review from a team as a code owner April 15, 2026 09:22
@eojindesu eojindesu added 🛠️ Refactor 코드 리팩토링, QA 반영 📈 Perf 성능 최적화 & UX 개선 labels Apr 15, 2026
@eojindesu
eojindesu requested review from miloupark and removed request for a team April 15, 2026 09:22
@eojindesu eojindesu linked an issue Apr 15, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

모임 상세 페이지 성능을 최적화하기 위해 컴포넌트 구조를 재설계했습니다. 큰 컴포넌트를 작은 단위로 분해하고, 메모이제이션을 추가하며, 권장 모임 섹션을 별도 async 컴포넌트로 이전했습니다.

Changes

Cohort / File(s) Summary
UI 컴포넌트 추가
apps/web/src/_pages/moim-detail/ui/scroll-reset.tsx, hero-image-section.tsx, recommended-meetings-wrapper.tsx
마운트 시 스크롤을 맨 위로 초기화하는 ScrollReset 컴포넌트, 반응형 히어로 이미지 컨테이너를 렌더링하는 HeroImageSection, 그리고 SSR을 비활성화하여 RecommendedMeetingsClient를 동적으로 로드하는 래퍼 추가
권장 모임 컴포넌트
apps/web/src/_pages/moim-detail/ui/recommended-meetings-client.tsx, recommended-meetings-section.tsx
권장 모임 카드를 렌더링하고 좋아요 상태를 관리하는 RecommendedMeetingsClient, 모임 정보를 가져와서 필터링/정렬하고 Suspense로 감싸는 RecommendedMeetingsSection (async) 추가
성능 최적화
apps/web/src/_pages/moim-detail/ui/information-container.tsx
ActionButton과 InformationContainer에 memo 적용, tagItems/actionButtonProps를 useMemo로 메모이제이션, 모든 핸들러를 useCallback으로 래핑, displayName 추가
컴포넌트 리팩토링
apps/web/src/_pages/moim-detail/ui/moim-detail-client.tsx
initialDescription/initialRecommendedMeetings props 제거, 스크롤 리셋 및 RESET 액션 useEffect 삭제, 뒤로가기 버튼/설명/권장 모임 UI 제거, 핸들러를 useCallback으로 최적화
페이지 구조 개선
apps/web/src/app/(main)/moim-detail/[meetingId]/page.tsx
getAllMeetings 로직 제거, Promise.allSettled에서 권장 모임 페칭 삭제, 새로운 레이아웃 구조 (ScrollReset, HeroImageSection, DescriptionSection, RecommendedMeetingsSection) 도입, 초기화 props 간소화
정렬 로직 업데이트
apps/web/src/entities/moim-detail/model/sort-recommended-meetings.ts
isOpenMeeting 헬퍼 추가, registrationEnd 기반으로 모임 개방 여부 판단, 정렬 시 열려있는 모임을 우선순위로 처리

Sequence Diagram(s)

sequenceDiagram
    participant Page as MoimDetailPage
    participant Section as RecommendedMeetingsSection
    participant Server as Server
    participant Parser as parseMeetingsPage
    participant Sort as sortRecommendedMeetings
    participant Wrapper as RecommendedMeetingsWrapper
    participant Client as RecommendedMeetingsClient

    Page->>Section: meetingId 전달
    Section->>Server: 모임 상세 정보 요청
    Server-->>Section: meetingDetail 응답
    Section->>Server: 모든 모임 목록 페칭 (커서 기반)
    Server-->>Section: 모임 데이터 응답
    Section->>Parser: 응답 데이터 정규화
    Parser-->>Section: 파싱된 모임 배열
    Section->>Sort: 현재 모임 정보 기반 정렬
    Sort-->>Section: 정렬된 권장 모임
    Section->>Wrapper: 권장 모임 데이터 전달
    Wrapper->>Client: 클라이언트 렌더링
    Client-->>Page: 권장 모임 UI 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • miloupark
  • chan-byeong

Poem

🐰✨ 페이지를 쪼개고 메모하고, 권장 모임은 따로따로,
스크롤은 위로 초 빠르게, 히어로 이미지가 멋지게,
성능 최적화의 마법으로, 모임 상세가 더 빠르고 가볍네! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed 모든 코드 변경 사항이 이슈 #222의 성능 최적화 목표를 충족합니다. 이미지 최적화, Suspense 기반 지연 로딩, 불필요한 Effect 제거, 메모이제이션, 정렬 개선 등이 구현되어 LCP 및 Performance Score 향상을 달성했습니다.
Out of Scope Changes check ✅ Passed 모든 코드 변경이 성능 최적화 범위 내에 있으며 관련 없는 기능은 포함되지 않았습니다. 제거된 기능(초기 권장 미팅, 뒤로가기 버튼)도 성능 개선을 위한 의도적인 리팩토링입니다.

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


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/_pages/moim-detail/ui/recommended-meetings-client.tsx`:
- Around line 90-96: The inline onLike prop creates a new function each render
causing potential re-renders of CompactCard; wrap the onLike handler in
useCallback (e.g. const onLike = useCallback(() => { if (isPending) return
false; return handleToggleLike(meeting.id, meeting.isLiked); }, [isPending,
meeting.id, meeting.isLiked, handleToggleLike])) and pass that onLike to
CompactCard, and also ensure handleToggleLike itself is memoized with
useCallback so its reference is stable.

In `@apps/web/src/_pages/moim-detail/ui/recommended-meetings-section.tsx`:
- Around line 136-137: The catch block in recommended-meetings-section.tsx
currently swallows all exceptions (catch { return null; }), so change it to
capture the exception (catch (err)) and emit a structured error log before
returning null; use the project's logger (or console.error if none) and include
context keys such as component: "recommended-meetings-section", any relevant IDs
or props (e.g., moimId or userId) and the error object to aid triage, e.g.,
logger.error({ err, component: "recommended-meetings-section", moimId, props },
"Failed to load recommended meetings") then return null as before.
- Around line 76-88: getAllMeetings can loop forever if the API returns the same
cursor repeatedly; update getAllMeetings to guard against repeated/unchanged
cursors by tracking seen cursors (e.g., a Set) or comparing the new nextCursor
to the previous cursor and breaking/throwing if it repeats or is unchanged, and
optionally enforce a max-iteration safety limit; reference getAllMeetings,
cursor, nextCursor, parseMeetingsPage, and isRecommendedMeetingItem when
applying the fix.
- Around line 93-133: RecommendedMeetingsSection currently does all data
fetching (getApi, getAllMeetings, apiClient.meetings.getDetail,
sortRecommendedMeetings, mapMeetingToRecommendedMeetingData) before rendering,
causing the section title to wait; extract the async logic into a new async
child component (e.g., RecommendedMeetingsLoader or RecommendedMeetingsContent)
that performs getApi, getAllMeetings, apiClient.meetings.getDetail, builds
sortableMeetingDetail, calls sortRecommendedMeetings and
mapMeetingToRecommendedMeetingData and returns <RecommendedMeetingsWrapper
meetings={...}>; make RecommendedMeetingsSection synchronous so it immediately
renders the static title and a Suspense that lazy-loads the new async child;
keep the existing Suspense fallback UI and ensure you reference
RecommendedMeetingsWrapper from the child.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 1ee6af79-ec5a-4d49-9278-a28af8cffcaa

📥 Commits

Reviewing files that changed from the base of the PR and between c51b286 and 9fe55c7.

📒 Files selected for processing (9)
  • apps/web/src/_pages/moim-detail/lib/scroll-reset.tsx
  • apps/web/src/_pages/moim-detail/ui/hero-image-section.tsx
  • apps/web/src/_pages/moim-detail/ui/information-container.tsx
  • apps/web/src/_pages/moim-detail/ui/moim-detail-client.tsx
  • apps/web/src/_pages/moim-detail/ui/recommended-meetings-client.tsx
  • apps/web/src/_pages/moim-detail/ui/recommended-meetings-section.tsx
  • apps/web/src/_pages/moim-detail/ui/recommended-meetings-wrapper.tsx
  • apps/web/src/app/(main)/moim-detail/[meetingId]/page.tsx
  • apps/web/src/entities/moim-detail/model/sort-recommended-meetings.ts

Comment on lines +90 to +96
onLike={() => {
if (isPending) {
return false;
}

return handleToggleLike(meeting.id, meeting.isLiked);
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

인라인 함수를 useCallback으로 메모이제이션하는 것을 고려해 보세요.

CompactCard가 메모이제이션된 컴포넌트라면, 매 렌더링마다 새로운 onLike 함수 참조가 생성되어 자식 컴포넌트의 불필요한 리렌더링을 유발할 수 있습니다. 성능에 민감한 리스트에서는 useCallback을 사용하여 함수 참조를 안정화하는 것이 좋습니다.

♻️ 선택적 리팩토링 제안
+ const handleLikeForMeeting = useCallback(
+   (meetingId: number, isLiked: boolean) => {
+     if (pendingLikeIds.includes(meetingId)) {
+       return false;
+     }
+     return handleToggleLike(meetingId, isLiked);
+   },
+   [pendingLikeIds, handleToggleLike],
+ );

그리고 JSX에서:

  onLike={() => {
-   if (isPending) {
-     return false;
-   }
-   return handleToggleLike(meeting.id, meeting.isLiked);
+   return handleLikeForMeeting(meeting.id, meeting.isLiked);
  }}

단, handleToggleLike 함수도 useCallback으로 래핑해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/_pages/moim-detail/ui/recommended-meetings-client.tsx` around
lines 90 - 96, The inline onLike prop creates a new function each render causing
potential re-renders of CompactCard; wrap the onLike handler in useCallback
(e.g. const onLike = useCallback(() => { if (isPending) return false; return
handleToggleLike(meeting.id, meeting.isLiked); }, [isPending, meeting.id,
meeting.isLiked, handleToggleLike])) and pass that onLike to CompactCard, and
also ensure handleToggleLike itself is memoized with useCallback so its
reference is stable.

Comment on lines +76 to +88
const getAllMeetings = async (apiClient: Awaited<ReturnType<typeof getApi>>) => {
const allMeetings: RecommendedMeetingItem[] = [];
let cursor: string | null = null;

while (true) {
const response = await apiClient.meetings.getList(cursor ? { cursor } : undefined);
const { data, nextCursor } = parseMeetingsPage<RecommendedMeetingItem>(response);

allMeetings.push(...data.filter(isRecommendedMeetingItem));

if (!nextCursor) break;
cursor = nextCursor;
}

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 | 🔴 Critical

커서 순환 응답 시 while (true)가 종료되지 않을 수 있습니다.

Line 80~88은 nextCursor만으로 탈출하는 구조라, API가 동일 커서를 반복 반환하면 요청이 끝나지 않고 서버 렌더링이 매달릴 수 있습니다.

🔧 제안 수정안
 const getAllMeetings = async (apiClient: Awaited<ReturnType<typeof getApi>>) => {
   const allMeetings: RecommendedMeetingItem[] = [];
   let cursor: string | null = null;
+  const seenCursors = new Set<string>();
+  let pageCount = 0;
+  const MAX_PAGES = 100;

   while (true) {
+    if (pageCount++ >= MAX_PAGES) break;
+    if (cursor && seenCursors.has(cursor)) break;
+    if (cursor) seenCursors.add(cursor);
+
     const response = await apiClient.meetings.getList(cursor ? { cursor } : undefined);
     const { data, nextCursor } = parseMeetingsPage<RecommendedMeetingItem>(response);

     allMeetings.push(...data.filter(isRecommendedMeetingItem));

-    if (!nextCursor) break;
+    if (!nextCursor || nextCursor === cursor) break;
     cursor = nextCursor;
   }

   return allMeetings;
 };
📝 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
const getAllMeetings = async (apiClient: Awaited<ReturnType<typeof getApi>>) => {
const allMeetings: RecommendedMeetingItem[] = [];
let cursor: string | null = null;
while (true) {
const response = await apiClient.meetings.getList(cursor ? { cursor } : undefined);
const { data, nextCursor } = parseMeetingsPage<RecommendedMeetingItem>(response);
allMeetings.push(...data.filter(isRecommendedMeetingItem));
if (!nextCursor) break;
cursor = nextCursor;
}
const getAllMeetings = async (apiClient: Awaited<ReturnType<typeof getApi>>) => {
const allMeetings: RecommendedMeetingItem[] = [];
let cursor: string | null = null;
const seenCursors = new Set<string>();
let pageCount = 0;
const MAX_PAGES = 100;
while (true) {
if (pageCount++ >= MAX_PAGES) break;
if (cursor && seenCursors.has(cursor)) break;
if (cursor) seenCursors.add(cursor);
const response = await apiClient.meetings.getList(cursor ? { cursor } : undefined);
const { data, nextCursor } = parseMeetingsPage<RecommendedMeetingItem>(response);
allMeetings.push(...data.filter(isRecommendedMeetingItem));
if (!nextCursor || nextCursor === cursor) break;
cursor = nextCursor;
}
return allMeetings;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/_pages/moim-detail/ui/recommended-meetings-section.tsx` around
lines 76 - 88, getAllMeetings can loop forever if the API returns the same
cursor repeatedly; update getAllMeetings to guard against repeated/unchanged
cursors by tracking seen cursors (e.g., a Set) or comparing the new nextCursor
to the previous cursor and breaking/throwing if it repeats or is unchanged, and
optionally enforce a max-iteration safety limit; reference getAllMeetings,
cursor, nextCursor, parseMeetingsPage, and isRecommendedMeetingItem when
applying the fix.

Comment on lines +93 to +133
export async function RecommendedMeetingsSection({ meetingId }: Props) {
const apiClient = await getApi();

try {
const [meetingDetailResponse, allMeetings] = await Promise.all([
apiClient.meetings.getDetail(meetingId),
getAllMeetings(apiClient),
]);

const meetingDetail = "data" in meetingDetailResponse ? meetingDetailResponse.data : meetingDetailResponse;

if (!meetingDetail) return null;

const sortableMeetingDetail: CurrentMeetingSortable = {
id: meetingDetail.id,
type: meetingDetail.type,
region: meetingDetail.region,
hostId: meetingDetail.hostId,
};

const recommendedMeetings = sortRecommendedMeetings(allMeetings, sortableMeetingDetail).map(
mapMeetingToRecommendedMeetingData,
);

if (recommendedMeetings.length === 0) return null;

return (
<section className="mx-auto flex w-full max-w-6xl flex-col gap-4 pb-20">
<h2 className="font-semibold text-black text-xl leading-[1.4]">이런 모임은 어때요?</h2>

<Suspense
fallback={
<div className="grid grid-cols-2 gap-4 xl:grid-cols-4">
{[1, 2, 3, 4].map((num) => (
<div key={num} className="aspect-[162/114] w-full animate-pulse rounded-xl bg-slate-100" />
))}
</div>
}
>
<RecommendedMeetingsWrapper meetings={recommendedMeetings} />
</Suspense>

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) page.tsx에서 RecommendedMeetingsSection 주위 Suspense 경계 확인"
rg -n -C3 'RecommendedMeetingsSection|<Suspense' apps/web/src/app

echo
echo "2) recommended-meetings-wrapper.tsx의 client/dynamic(ssr) 설정 확인"
fd -i 'recommended-meetings-wrapper.tsx' apps/web/src | xargs -I{} sh -c 'echo "--- {} ---"; sed -n "1,220p" "{}"'

echo
echo "3) recommended-meetings-section.tsx의 await 위치 확인"
sed -n '90,180p' apps/web/src/_pages/moim-detail/ui/recommended-meetings-section.tsx

Repository: sprint-13/moum-zip

Length of output: 12271


🏁 Script executed:

#!/bin/bash

# Check if MoimDetailContent is async and understand parent structure
echo "=== MoimDetailContent component structure ==="
sed -n '115,170p' apps/web/src/app/(main)/moim-detail/\[meetingId\]/page.tsx

echo
echo "=== Check for any Suspense wrapping RecommendedMeetingsSection ==="
rg -B5 -A5 'RecommendedMeetingsSection' apps/web/src/app/(main)/moim-detail/\[meetingId\]/page.tsx

Repository: sprint-13/moum-zip

Length of output: 288


🏁 Script executed:

#!/bin/bash

# Check MoimDetailContent component - use quotes to handle brackets
echo "=== MoimDetailContent component structure ==="
sed -n '115,170p' 'apps/web/src/app/(main)/moim-detail/[meetingId]/page.tsx'

echo
echo "=== Full context around RecommendedMeetingsSection ==="
sed -n '130,160p' 'apps/web/src/app/(main)/moim-detail/[meetingId]/page.tsx'

Repository: sprint-13/moum-zip

Length of output: 2974


섹션 제목을 독립적으로 스트리밍하려면 비동기 로직 분리가 필요합니다.

현재 구조에서는 RecommendedMeetingsSection이 모든 데이터를 fetch하고 난 후에 JSX를 반환하므로, 라인 123의 내부 Suspense는 카드 영역 로딩에만 관여합니다. 더불어 RecommendedMeetingsSectionMoimDetailContent 내에서 호출되고 부모 페이지에 Suspense 경계가 전체를 감싸고 있어서, 이 컴포넌트의 데이터 로딩이 부모의 풀스크린 로더 표시를 유발합니다. 결과적으로 제목은 모든 API 요청이 완료될 때까지 화면에 나타나지 않습니다.

타이틀과 카드 영역의 로딩을 분리하려면 비동기 로직을 자식 컴포넌트로 추출하고 섹션 제목은 동기 렌더링으로 변경해야 합니다:

제안하는 구조 변경
-export async function RecommendedMeetingsSection({ meetingId }: Props) {
-  const apiClient = await getApi();
-  try {
-    const [meetingDetailResponse, allMeetings] = await Promise.all([
-      apiClient.meetings.getDetail(meetingId),
-      getAllMeetings(apiClient),
-    ]);
-    // ... 로직
-    return (
-      <section ...>
-        <h2>이런 모임은 어때요?</h2>
-        <Suspense fallback={...}>
-          <RecommendedMeetingsWrapper meetings={recommendedMeetings} />
-        </Suspense>
-      </section>
-    );
-  } catch {
-    return null;
-  }
-}
+export function RecommendedMeetingsSection({ meetingId }: Props) {
+  return (
+    <section className="mx-auto flex w-full max-w-6xl flex-col gap-4 pb-20">
+      <h2 className="font-semibold text-black text-xl leading-[1.4]">이런 모임은 어때요?</h2>
+      <Suspense fallback={
+        <div className="grid grid-cols-2 gap-4 xl:grid-cols-4">
+          {[1, 2, 3, 4].map((num) => (
+            <div key={num} className="aspect-[162/114] w-full animate-pulse rounded-xl bg-slate-100" />
+          ))}
+        </div>
+      }>
+        <RecommendedMeetingsContent meetingId={meetingId} />
+      </Suspense>
+    </section>
+  );
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/_pages/moim-detail/ui/recommended-meetings-section.tsx` around
lines 93 - 133, RecommendedMeetingsSection currently does all data fetching
(getApi, getAllMeetings, apiClient.meetings.getDetail, sortRecommendedMeetings,
mapMeetingToRecommendedMeetingData) before rendering, causing the section title
to wait; extract the async logic into a new async child component (e.g.,
RecommendedMeetingsLoader or RecommendedMeetingsContent) that performs getApi,
getAllMeetings, apiClient.meetings.getDetail, builds sortableMeetingDetail,
calls sortRecommendedMeetings and mapMeetingToRecommendedMeetingData and returns
<RecommendedMeetingsWrapper meetings={...}>; make RecommendedMeetingsSection
synchronous so it immediately renders the static title and a Suspense that
lazy-loads the new async child; keep the existing Suspense fallback UI and
ensure you reference RecommendedMeetingsWrapper from the child.

Comment on lines +136 to +137
} catch {
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

예외를 무음 처리하면 성능/데이터 장애 추적이 어려워집니다.

Line 136~137에서 모든 예외를 null로 삼키고 있어, 추천 섹션 미노출의 원인(API 실패/파싱 실패/데이터 품질)을 운영에서 식별하기 어렵습니다. 최소한 구조화 로그는 남겨 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/_pages/moim-detail/ui/recommended-meetings-section.tsx` around
lines 136 - 137, The catch block in recommended-meetings-section.tsx currently
swallows all exceptions (catch { return null; }), so change it to capture the
exception (catch (err)) and emit a structured error log before returning null;
use the project's logger (or console.error if none) and include context keys
such as component: "recommended-meetings-section", any relevant IDs or props
(e.g., moimId or userId) and the error object to aid triage, e.g.,
logger.error({ err, component: "recommended-meetings-section", moimId, props },
"Failed to load recommended meetings") then return null as before.

@miloupark miloupark left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

PR에 구조 분리랑 개선 방향을 꼼꼼하게 적어주셔서 내용 파악하기 좋았습니다!
코드래빗 코멘트만 반영되면 더 좋을 것 같습니다:)
고생하셨습니다! 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📈 Perf 성능 최적화 & UX 개선 🛠️ Refactor 코드 리팩토링, QA 반영

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[refactor] 모임 상세 페이지 성능 최적화

2 participants