Skip to content

Conversation

@jjangminii
Copy link
Collaborator

@jjangminii jjangminii commented Dec 17, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

  • 서스펜스쿼리로 변경
  • 로딩바운더리 적용
  • 에러바운더리 적용
  • 서스펜스 사용을 위해 카드 렌더링 부분 파일 분리

⭐ PR Point (To Reviewer)

처음해보는거라 공부할 것도 많고 문제도 많았어요 그중에 이슈 있었던 부분 아티클로 정리해둬서 글 링크로 첨부합니다
만약 Suspense 바깥에서 동일한 API를 불러온다면?

📷 Screenshot

2025-12-17.4.55.35.mov

Summary by CodeRabbit

  • New Features
    • 북마크 콘텐츠가 별도 컴포넌트로 분리되어 전체/안 읽음 토글, 무한 스크롤, 카드 목록 및 빈 상태가 개선되었습니다.
  • 개선 사항
    • 북마크 페이지에 로딩 화면과 오류 화면 경계가 추가되어 네트워크/로딩 상황 처리 품질 향상.
    • 북마크 읽음 처리 후 목록 갱신 및 모달(편집/삭제) 흐름이 안정화되었습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@jjangminii jjangminii self-assigned this Dec 17, 2025
@jjangminii jjangminii linked an issue Dec 17, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Dec 17, 2025

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

Project Deployment Review Updated (UTC)
pinback-client-client Ready Ready Preview, Comment Dec 17, 2025 3:53pm
pinback-client-landing Ready Ready Preview, Comment Dec 17, 2025 3:53pm

@github-actions github-actions bot added the refactor 코드 리팩토링 label Dec 17, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

북마크 페이지를 Suspense + ErrorBoundary 아키텍처로 리팩토링하고, 쿼리 훅을 useSuspenseInfiniteQuery로 전환하며, 렌더링 로직을 신규 MyBookmarkContent 컴포넌트로 분리했습니다. 로딩/에러 바운더리 컴포넌트와 의존성 하나를 추가했습니다.

Changes

Cohort / File(s) Summary
의존성 업데이트
apps/client/package.json
react-error-boundary ^6.0.0 추가
쿼리 훅 리팩토링
apps/client/src/pages/myBookmark/apis/queries.ts
useInfiniteQueryuseSuspenseInfiniteQuery로 변경(3개 훅); next-page 파라미터 간결화; useGetCategoryBookmarkArticles에서 categoryId가 falsy면 null 반환 및 getNextPageParam 가드 추가
메인 페이지 리팩토링
apps/client/src/pages/myBookmark/MyBookmark.tsx
데이터 페칭·페이지네이션 로직을 MyBookmarkContent로 위임하고 Suspense/ErrorBoundary로 감싸며, 삭제/수정 모달 상태는 유지
새로운 콘텐츠 컴포넌트
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx
북마크 아티클 집계·필터링(전체/안읽음/카테고리), 무한 스크롤 트리거, 카드 그리드 렌더링, 읽음 상태 업데이트 및 쿼리 무효화 담당 (신규 기본 export)
바운더리 컴포넌트
apps/client/src/shared/components/articlesErrorBoundary/ArticlesErrorBoundary.tsx, apps/client/src/shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary.tsx
에러(404) 및 로딩 상태용 UI 컴포넌트 추가

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 집중 검토 필요 항목:
    • MyBookmarkContent의 데이터 집계/필터 로직(배지 상태 및 카테고리 분기)
    • useSuspenseInfiniteQuery 전환에 따른 에러/로딩 흐름 변화와 쿼리 무효화 타이밍
    • useGetCategoryBookmarkArticlesnull 반환 처리 및 호출부 호환성
    • MyBookmark에서 모달 상태(편집/삭제)와 Suspense 경계 간의 일관성

Possibly related PRs

Suggested labels

frontend

Suggested reviewers

  • jllee000
  • constantly-dev

Poem

🐰
Suspense 숲을 살금살금 지나,
경계선 위에 에러를 내려놓고,
북마크들 모아 새 둥지 지었네,
깡총깡총 코드 정리 기념 춤,
당근 한 입, 배포를 향해! 🎉

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 주요 변경 사항인 로딩/에러 바운더리 적용을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿 구조를 따르고 있으며, 관련 이슈, 작업 내용, 리뷰어 노트가 포함되어 있습니다.
Linked Issues check ✅ Passed PR의 변경 사항이 #208 이슈의 로딩/에러 바운더리 설정 목표를 충족하고 있습니다. 새로운 로딩 바운더리, 에러 바운더리 컴포넌트 구현과 Suspense 기반 쿼리 변경이 확인됩니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 #208의 로딩/에러 바운더리 설정과 관련된 범위 내에 있습니다. react-error-boundary 의존성 추가는 에러 바운더리 구현에 필요합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#208/loading-error-boundary

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6cbc7f3 and 4e7e714.

📒 Files selected for processing (1)
  • apps/client/src/shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/client/src/shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: storybook

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.

@github-actions
Copy link

github-actions bot commented Dec 17, 2025

✅ Storybook chromatic 배포 확인:
🐿️ storybook

Copy link

@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: 5

🧹 Nitpick comments (4)
apps/client/src/shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary.tsx (1)

6-10: 로딩 상태에 에러 이미지와 alt 텍스트 사용 중

TODO 주석에서 언급했듯이, 로딩 상태에 chippi_error.svg를 사용하고 있고 alt 텍스트도 "Error"로 되어 있습니다. 접근성과 의미론적 정확성을 위해 로딩 전용 이미지와 적절한 alt 텍스트(예: "Loading")로 변경이 필요합니다.

로딩 전용 이미지 에셋이 준비되면 이 이슈를 추적하기 위한 별도 이슈를 생성할까요?

apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

100-101: queryClient prop drilling 개선 가능

queryClient를 prop으로 전달하는 대신, MyBookmarkContent 내부에서 useQueryClient()를 직접 호출하는 것이 더 깔끔합니다. 이렇게 하면 컴포넌트 간 결합도가 낮아지고 prop 목록이 줄어듭니다.

apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx (2)

35-54: 불필요한 쿼리 실행을 방지하세요.

세 개의 쿼리가 모두 무조건 실행되지만, 실제로는 activeBadgecategory 상태에 따라 하나의 데이터만 사용됩니다. 사용하지 않는 쿼리도 Suspense를 트리거하여 불필요한 네트워크 요청과 로딩 상태를 유발할 수 있습니다.

조건부로 쿼리를 활성화하는 방식을 고려하세요:

 const {
   data: articlesData,
   fetchNextPage: fetchNextArticles,
   hasNextPage: hasNextArticles,
-} = useGetBookmarkArticles();
+} = useGetBookmarkArticles({
+  enabled: !category && activeBadge === 'all',
+});

 const {
   data: unreadArticlesData,
   fetchNextPage: fetchNextUnreadArticles,
   hasNextPage: hasNextUnreadArticles,
-} = useGetBookmarkUnreadArticles();
+} = useGetBookmarkUnreadArticles({
+  enabled: !category && activeBadge === 'notRead',
+});

 const {
   data: categoryArticlesData,
   fetchNextPage: fetchNextCategoryArticles,
   hasNextPage: hasNextCategoryArticles,
 } = useGetCategoryBookmarkArticles(
   categoryId,
-  activeBadge === 'notRead' ? false : null
+  activeBadge === 'notRead' ? false : null,
+  { enabled: !!categoryId }
 );

참고: 이를 위해 쿼리 훅에 enabled 옵션을 전달할 수 있도록 수정이 필요합니다.


56-59: null 처리를 더 명확하게 하세요.

useGetCategoryBookmarkArticlescategoryId가 null일 때 쿼리 함수에서 null을 반환합니다. 현재 옵셔널 체이닝으로 처리되고 있지만, 타입 안전성을 위해 더 명확한 처리를 권장합니다.

 const categoryList =
-  categoryId && categoryArticlesData?.pages
-    ? categoryArticlesData.pages.flatMap((page) => page.articles)
+  categoryId && categoryArticlesData?.pages && Array.isArray(categoryArticlesData.pages)
+    ? categoryArticlesData.pages.flatMap((page) => page?.articles ?? [])
     : [];
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07f6734 and 6cbc7f3.

⛔ Files ignored due to path filters (2)
  • apps/client/src/assets/chippi_error.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • apps/client/package.json (1 hunks)
  • apps/client/src/pages/myBookmark/MyBookmark.tsx (3 hunks)
  • apps/client/src/pages/myBookmark/apis/queries.ts (1 hunks)
  • apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx (1 hunks)
  • apps/client/src/shared/components/articlesErrorBoundary/ArticlesErrorBoundary.tsx (1 hunks)
  • apps/client/src/shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T09:18:13.818Z
Learnt from: constantly-dev
Repo: Pinback-Team/pinback-client PR: 102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.818Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.

Applied to files:

  • apps/client/src/pages/myBookmark/MyBookmark.tsx
🧬 Code graph analysis (2)
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx (2)
apps/client/src/pages/myBookmark/apis/queries.ts (3)
  • useGetBookmarkArticles (8-16)
  • useGetBookmarkUnreadArticles (18-26)
  • useGetCategoryBookmarkArticles (28-46)
apps/client/src/shared/hooks/useInfiniteScroll.ts (1)
  • useInfiniteScroll (15-51)
apps/client/src/pages/myBookmark/apis/queries.ts (1)
apps/client/src/pages/myBookmark/apis/axios.ts (3)
  • getBookmarkArticles (3-8)
  • getBookmarkUnreadArticles (10-15)
  • getCategoryBookmarkArticles (17-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: storybook
🔇 Additional comments (4)
apps/client/package.json (1)

20-20: LGTM!

react-error-boundary 라이브러리 추가가 적절합니다. React 19와 호환되는 최신 버전을 사용하고 있으며, Suspense 기반 에러 처리 패턴 구현에 적합한 선택입니다.

apps/client/src/pages/myBookmark/MyBookmark.tsx (1)

91-104: Suspense/ErrorBoundary 패턴 적용 승인

Suspense와 ErrorBoundary 조합이 올바르게 구성되어 있습니다. useSuspenseInfiniteQuery를 사용하는 MyBookmarkContent가 경계 내부에서 렌더링되어 로딩/에러 상태를 적절히 처리합니다.

apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx (2)

87-91: 무한 스크롤 구현이 올바릅니다.

useInfiniteScroll 훅의 사용이 적절하며, scrollContainerRefroot로 전달하는 패턴이 올바르게 구현되었습니다.


121-122: scrollContainerRef 사용 패턴이 올바르게 구현되어 있습니다.

scrollContainerRef는 부모 컴포넌트(MyBookmark.tsx)에서 useRef로 생성되어 (34줄) MyBookmarkContent에 props로 전달되며, 자식 컴포넌트에서 스크롤 컨테이너 요소(121줄)와 useInfiniteScroll 훅의 root 옵션(90줄)에 올바르게 사용됩니다. 표준 React 패턴을 따르고 있으므로 추가 조치가 필요하지 않습니다.

Comment on lines +32 to 45
return useSuspenseInfiniteQuery({
queryKey: ['categoryBookmarkArticles', readStatus, categoryId],
queryFn: ({ pageParam = 0 }) =>
getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20),

queryFn: ({ pageParam = 0 }) => {
if (!categoryId) return null;
return getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20);
},

initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.articles.length === 0) {
return undefined;
}
if (!lastPage || lastPage.articles.length === 0) return undefined;
return allPages.length;
},
enabled: !!categoryId,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

useSuspenseInfiniteQuery에서 categoryId 부재 시 skipToken 사용

TypeScript 사용자는 enabled: false의 대안으로 skipToken을 사용할 수 있으며, 이는 조건에 따라 쿼리를 비활성화하면서도 타입 안전성을 유지하는 데 유용합니다. TanStack Query v5에서 useSuspenseInfiniteQueryskipToken을 queryFn으로 지원합니다.

현재 코드에서 categoryId가 없을 때 null을 반환하는 것보다 queryFn에 조건부 연산자를 사용하여 categoryId ? () => getCategoryBookmarkArticles(...) : skipToken 패턴을 적용하면, 쿼리가 실행되지 않아 더 안전합니다. 이를 통해 getNextPageParam에서 null 값 처리 가드를 제거할 수 있습니다.

+import { useSuspenseInfiniteQuery, skipToken } from '@tanstack/react-query';

 export const useGetCategoryBookmarkArticles = (
   categoryId: string | null,
   readStatus: boolean | null
 ) => {
   return useSuspenseInfiniteQuery({
     queryKey: ['categoryBookmarkArticles', readStatus, categoryId],
-    queryFn: ({ pageParam = 0 }) => {
-      if (!categoryId) return null;
-      return getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20);
-    },
+    queryFn: categoryId
+      ? ({ pageParam = 0 }) =>
+          getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20)
+      : skipToken,
     initialPageParam: 0,
     getNextPageParam: (lastPage, allPages) => {
-      if (!lastPage || lastPage.articles.length === 0) return undefined;
+      if (lastPage.articles.length === 0) return undefined;
       return allPages.length;
     },
   });
 };
📝 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
return useSuspenseInfiniteQuery({
queryKey: ['categoryBookmarkArticles', readStatus, categoryId],
queryFn: ({ pageParam = 0 }) =>
getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20),
queryFn: ({ pageParam = 0 }) => {
if (!categoryId) return null;
return getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20);
},
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.articles.length === 0) {
return undefined;
}
if (!lastPage || lastPage.articles.length === 0) return undefined;
return allPages.length;
},
enabled: !!categoryId,
});
return useSuspenseInfiniteQuery({
queryKey: ['categoryBookmarkArticles', readStatus, categoryId],
queryFn: categoryId
? ({ pageParam = 0 }) =>
getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20)
: skipToken,
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.articles.length === 0) return undefined;
return allPages.length;
},
});

onBadgeChange: (type: 'all' | 'notRead') => void;
category: string | null;
categoryId: string | null;
updateToReadStatus: (id: number, options?: any) => void;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

타입 안전성을 위해 any 타입을 구체적인 타입으로 교체하세요.

Line 19의 options?: any와 Line 21의 queryClient: any는 타입 안전성을 저하시킵니다. QueryClient 타입(from @tanstack/react-query)과 mutation 옵션의 구체적인 타입을 사용하세요.

다음 diff를 적용하여 타입을 개선하세요:

+import { QueryClient } from '@tanstack/react-query';
+
 interface MyBookmarkContentProps {
   activeBadge: 'all' | 'notRead';
   onBadgeChange: (type: 'all' | 'notRead') => void;
   category: string | null;
   categoryId: string | null;
-  updateToReadStatus: (id: number, options?: any) => void;
+  updateToReadStatus: (id: number, options?: { onSuccess?: () => void; onError?: (error: Error) => void }) => void;
   openMenu: (id: number, anchor: HTMLElement) => void;
-  queryClient: any;
+  queryClient: QueryClient;
   scrollContainerRef: MutableRefObject<HTMLDivElement | null>;
 }

Also applies to: 21-21

🤖 Prompt for AI Agents
In
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx
around lines 19 and 21, replace the loose any types with react-query types:
change options?: any to a concrete UseMutationOptions type (e.g.
UseMutationOptions<void, unknown, number, unknown> or another appropriate
generics matching your mutation result/error/variables/context) and change
queryClient: any to queryClient: QueryClient from @tanstack/react-query; import
QueryClient and UseMutationOptions at the top and update the function signature
to use these types so the mutation options and query client are type-safe.

Comment on lines +93 to +100
/** Empty 상태 컴포넌트 */
const EmptyStateComponent = () => {
if (articlesToDisplay.length === 0) {
if (articlesData?.pages?.[0]?.totalArticle === 0) return <NoArticles />;
return <NoUnreadArticles />;
}
return null;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Empty 상태 판단 로직이 잘못되었습니다.

Line 96에서 articlesData?.pages?.[0]?.totalArticle === 0을 확인하지만, articlesToDisplayactiveBadgecategory에 따라 다른 데이터 소스(unreadArticlesData 또는 categoryArticlesData)에서 올 수 있습니다. 이로 인해 잘못된 Empty 상태가 표시될 수 있습니다.

다음과 같이 수정하세요:

 const EmptyStateComponent = () => {
   if (articlesToDisplay.length === 0) {
-    if (articlesData?.pages?.[0]?.totalArticle === 0) return <NoArticles />;
+    if (totalArticle === 0) return <NoArticles />;
     return <NoUnreadArticles />;
   }
   return null;
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx
around lines 93 to 100, the EmptyStateComponent checks
articlesData?.pages?.[0]?.totalArticle which is wrong because articlesToDisplay
can come from unreadArticlesData or categoryArticlesData depending on
activeBadge/category; update the logic to inspect the correct source: if
activeBadge indicates "unread" use unreadArticlesData?.pages?.[0]?.totalArticle,
else if a category filter is active use
categoryArticlesData?.pages?.[0]?.totalArticle, otherwise fall back to
articlesData?.pages?.[0]?.totalArticle; return NoArticles when the chosen
source's totalArticle === 0, return NoUnreadArticles only when the unread source
is selected and its list is empty, otherwise return null.

Comment on lines +128 to +146
onClick={() => {
window.open(article.url, '_blank');
updateToReadStatus(article.articleId, {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['bookmarkReadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['bookmarkUnreadArticles'],
});
queryClient.invalidateQueries({
queryKey: ['categoryBookmarkArticles'],
});
queryClient.invalidateQueries({ queryKey: ['arcons'] });
},
onError: (error: any) => {
console.error(error);
},
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

쿼리 무효화 로직을 개선하고 에러 처리를 정리하세요.

현재 구현에는 몇 가지 개선 가능한 부분이 있습니다:

  1. 하드코딩된 쿼리 키: Lines 133, 136, 139, 141의 쿼리 키가 하드코딩되어 있어 queries.ts의 실제 쿼리 키와 불일치할 위험이 있습니다.
  2. 프로덕션 환경의 console.error: Line 144의 console.error는 프로덕션 환경에서 적절하지 않을 수 있습니다.
  3. 에러 토스트/알림 누락: 사용자에게 오류를 알리는 UI 피드백이 없습니다.

다음과 같이 개선하세요:

+// queries.ts에서 쿼리 키를 export
+export const QUERY_KEYS = {
+  bookmarkReadArticles: ['bookmarkReadArticles'],
+  bookmarkUnreadArticles: ['bookmarkUnreadArticles'],
+  categoryBookmarkArticles: ['categoryBookmarkArticles'],
+  arcons: ['arcons'],
+} as const;
 onClick={() => {
   window.open(article.url, '_blank');
   updateToReadStatus(article.articleId, {
     onSuccess: () => {
-      queryClient.invalidateQueries({
-        queryKey: ['bookmarkReadArticles'],
-      });
-      queryClient.invalidateQueries({
-        queryKey: ['bookmarkUnreadArticles'],
-      });
-      queryClient.invalidateQueries({
-        queryKey: ['categoryBookmarkArticles'],
-      });
-      queryClient.invalidateQueries({ queryKey: ['arcons'] });
+      queryClient.invalidateQueries({
+        queryKey: QUERY_KEYS.bookmarkReadArticles,
+      });
+      queryClient.invalidateQueries({
+        queryKey: QUERY_KEYS.bookmarkUnreadArticles,
+      });
+      queryClient.invalidateQueries({
+        queryKey: QUERY_KEYS.categoryBookmarkArticles,
+      });
+      queryClient.invalidateQueries({ 
+        queryKey: QUERY_KEYS.arcons 
+      });
     },
     onError: (error: any) => {
-      console.error(error);
+      // TODO: 사용자에게 에러 토스트 표시
+      if (process.env.NODE_ENV === 'development') {
+        console.error('Failed to update read status:', error);
+      }
     },
   });
 }}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/client/src/pages/myBookmark/components/myBookmarkContent/MyBookmarkContent.tsx
around lines 128 to 146, replace the hardcoded invalidateQueries keys with the
centralized query key constants exported from queries.ts (import the keys at top
and call queryClient.invalidateQueries with those constants) and remove the
console.error; instead surface errors to users via the app's toast/notification
helper (or logger) by calling the existing showErrorToast or toast.error with a
user-friendly message plus optional error details, and ensure success/error
handlers consistently invalidate the relevant query constants so UI state
updates reliably.

Comment on lines +3 to +18
const ArticlesErrorBoundary = () => {
return (
<div className="mt-[10rem] flex flex-col items-center justify-center text-center">
<h1 className="text-main500 head1 mb-[1rem]">404 ERROR</h1>

<p className="body1-m text-font-gray-3 mb-[3rem]">
죄송합니다. 페이지를 찾을 수 없습니다.
</p>

<img
src={chippiError}
alt="Error"
className="mt-[1rem] h-auto w-[18rem]"
/>
</div>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

에러 폴백 컴포넌트에서 에러 복구 기능 누락

ErrorBoundaryFallbackComponenterrorresetErrorBoundary props를 받습니다. 현재 구현에서는 이를 활용하지 않아 사용자가 에러 상태에서 복구할 수 없습니다.

또한 "404 ERROR"는 HTTP 상태 코드를 의미하는데, 이 컴포넌트는 JavaScript 런타임 에러나 쿼리 실패를 처리하므로 메시지가 혼란을 줄 수 있습니다.

+import { FallbackProps } from 'react-error-boundary';
 import chippiError from '@assets/chippi_error.svg';

-const ArticlesErrorBoundary = () => {
+const ArticlesErrorBoundary = ({ resetErrorBoundary }: FallbackProps) => {
   return (
     <div className="mt-[10rem] flex flex-col items-center justify-center text-center">
-      <h1 className="text-main500 head1 mb-[1rem]">404 ERROR</h1>
+      <h1 className="text-main500 head1 mb-[1rem]">오류 발생</h1>

       <p className="body1-m text-font-gray-3 mb-[3rem]">
-        죄송합니다. 페이지를 찾을 수 없습니다.
+        죄송합니다. 데이터를 불러오는 중 문제가 발생했습니다.
       </p>

       <img
         src={chippiError}
         alt="Error"
         className="mt-[1rem] h-auto w-[18rem]"
       />
+
+      <button
+        onClick={resetErrorBoundary}
+        className="mt-[2rem] rounded-lg bg-main500 px-[2rem] py-[1rem] text-white"
+      >
+        다시 시도
+      </button>
     </div>
   );
 };
🤖 Prompt for AI Agents
In
apps/client/src/shared/components/articlesErrorBoundary/ArticlesErrorBoundary.tsx
around lines 3 to 18, the component currently ignores the ErrorBoundary
FallbackComponent props and shows a misleading "404 ERROR" message; update the
component signature to accept the standard FallbackComponent props (error and
resetErrorBoundary), replace the "404 ERROR" heading with a generic runtime
error message (e.g., "Something went wrong"), optionally render a sanitized
error.message for debugging, and add a visible "Retry" button that calls
resetErrorBoundary when clicked so users can recover from the error state.

Copy link
Collaborator

@jllee000 jllee000 left a comment

Choose a reason for hiding this comment

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

Suspense관련 적어주신 아티클로 공부 많이 됐습니다! 로딩 바운더리의 신...! 고생하셨습니다

Comment on lines +9 to +14
return useSuspenseInfiniteQuery({
queryKey: ['bookmarkReadArticles'],
queryFn: ({ pageParam = 0 }) => getBookmarkArticles(pageParam, 20),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => {
if (lastPage.articles.length === 0) {
return undefined;
}
return allPages.length;
},
getNextPageParam: (lastPage, allPages) =>
lastPage.articles.length === 0 ? undefined : allPages.length,
Copy link
Collaborator

Choose a reason for hiding this comment

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

suspenseInfiniteQuery는 진심으로 처음 봤는데,, tanstack이 지원해주는게 참 많구만요

@jjangminii jjangminii merged commit 1e35eec into develop Dec 22, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 로딩/에러바운더리

3 participants