Skip to content

Commit 1e35eec

Browse files
authored
Refactor(client): loading error boundary 적용 (#214)
* feat: 바운더리 뷰 제작 * chore: 주석 정리 * refactor: useSuspenseInfiniteQuery로 변경하여 쿼리 최적화 * refactor: MyBookmarkContent 컴포넌트 추가 및 바운더리 적용 * refactor: ArticlesLoadingBoundary 로딩컨포넌트로 변경
1 parent 52d5d81 commit 1e35eec

File tree

8 files changed

+261
-160
lines changed

8 files changed

+261
-160
lines changed

apps/client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"framer-motion": "^12.23.12",
1818
"react": "^19.1.1",
1919
"react-dom": "^19.1.1",
20+
"react-error-boundary": "^6.0.0",
2021
"react-router-dom": "^7.8.2"
2122
},
2223
"devDependencies": {

apps/client/src/assets/chippi_error.svg

Lines changed: 9 additions & 0 deletions
Loading

apps/client/src/pages/myBookmark/MyBookmark.tsx

Lines changed: 21 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
1-
import { Badge, PopupContainer } from '@pinback/design-system/ui';
2-
import { useState, useRef } from 'react';
3-
import {
4-
useGetBookmarkArticles,
5-
useGetBookmarkUnreadArticles,
6-
useGetCategoryBookmarkArticles,
7-
} from '@pages/myBookmark/apis/queries';
1+
import { PopupContainer } from '@pinback/design-system/ui';
2+
import { useState, useRef, Suspense } from 'react';
83
import { useSearchParams } from 'react-router-dom';
94
import { REMIND_MOCK_DATA } from '@pages/remind/constants';
105
import CardEditModal from '@shared/components/cardEditModal/CardEditModal';
116
import OptionsMenuPortal from '@shared/components/sidebar/OptionsMenuPortal';
127
import { useAnchoredMenu } from '@shared/hooks/useAnchoredMenu';
138
import { belowOf } from '@shared/utils/anchorPosition';
14-
import NoArticles from '@pages/myBookmark/components/NoArticles/NoArticles';
159
import { Icon } from '@pinback/design-system/icons';
1610
import { useQueryClient } from '@tanstack/react-query';
1711
import {
1812
useGetArticleDetail,
1913
useDeleteRemindArticle,
2014
usePutArticleReadStatus,
2115
} from '@shared/apis/queries';
22-
import NoUnreadArticles from '@pages/myBookmark/components/noUnreadArticles/NoUnreadArticles';
23-
import FetchCard from '@pages/myBookmark/components/fetchCard/FetchCard';
24-
import { useInfiniteScroll } from '@shared/hooks/useInfiniteScroll';
2516
import Tooltip from '@shared/components/tooltip/Tooltip';
17+
import ArticlesLoadingBoundary from '@shared/components/articlesLoadingBoundary/ArticlesLoadingBoundary';
18+
import ArticlesErrorBoundary from '@shared/components/articlesErrorBoundary/ArticlesErrorBoundary';
19+
import { ErrorBoundary } from 'react-error-boundary';
20+
import MyBookmarkContent from '@pages/myBookmark/components/myBookmarkContent/MyBookmarkContent';
2621

2722
const MyBookmark = () => {
2823
const [activeBadge, setActiveBadge] = useState<'all' | 'notRead'>('all');
@@ -32,35 +27,14 @@ const MyBookmark = () => {
3227

3328
const [searchParams] = useSearchParams();
3429
const queryClient = useQueryClient();
30+
3531
const category = searchParams.get('category');
3632
const categoryId = searchParams.get('id');
3733

3834
const scrollContainerRef = useRef<HTMLDivElement>(null);
3935

4036
const { mutate: updateToReadStatus } = usePutArticleReadStatus();
4137
const { mutate: deleteArticle } = useDeleteRemindArticle();
42-
43-
const {
44-
data: articlesData,
45-
fetchNextPage: fetchNextArticles,
46-
hasNextPage: hasNextArticles,
47-
} = useGetBookmarkArticles();
48-
49-
const {
50-
data: unreadArticlesData,
51-
fetchNextPage: fetchNextUnreadArticles,
52-
hasNextPage: hasNextUnreadArticles,
53-
} = useGetBookmarkUnreadArticles();
54-
55-
const {
56-
data: categoryArticlesData,
57-
fetchNextPage: fetchNextCategoryArticles,
58-
hasNextPage: hasNextCategoryArticles,
59-
} = useGetCategoryBookmarkArticles(
60-
categoryId,
61-
activeBadge === 'notRead' ? false : null
62-
);
63-
6438
const { mutate: getArticleDetail, data: articleDetail } =
6539
useGetArticleDetail();
6640

@@ -72,34 +46,9 @@ const MyBookmark = () => {
7246
containerRef,
7347
} = useAnchoredMenu((anchor) => belowOf(anchor, 8));
7448

75-
const articlesToDisplay = category
76-
? (categoryArticlesData?.pages.flatMap((page) => page.articles) ?? [])
77-
: activeBadge === 'all'
78-
? (articlesData?.pages.flatMap((page) => page.articles) ?? [])
79-
: (unreadArticlesData?.pages.flatMap((page) => page.articles) ?? []);
80-
81-
const hasNextPage = category
82-
? hasNextCategoryArticles
83-
: activeBadge === 'all'
84-
? hasNextArticles
85-
: hasNextUnreadArticles;
86-
87-
const fetchNextPage = category
88-
? fetchNextCategoryArticles
89-
: activeBadge === 'all'
90-
? fetchNextArticles
91-
: fetchNextUnreadArticles;
92-
93-
const observerRef = useInfiniteScroll({
94-
fetchNextPage,
95-
hasNextPage,
96-
root: scrollContainerRef,
97-
});
98-
9949
const handleDeleteArticle = (id: number) => {
10050
deleteArticle(id, {
10151
onSuccess: () => {
102-
// TODO: 쿼리키 팩토리 패턴 적용
10352
queryClient.invalidateQueries({ queryKey: ['bookmarkReadArticles'] });
10453
queryClient.invalidateQueries({ queryKey: ['bookmarkUnreadArticles'] });
10554
queryClient.invalidateQueries({
@@ -119,31 +68,6 @@ const MyBookmark = () => {
11968
const getBookmarkTitle = (id: number | null) =>
12069
id == null ? '' : (REMIND_MOCK_DATA.find((d) => d.id === id)?.title ?? '');
12170

122-
const handleBadgeClick = (badgeType: 'all' | 'notRead') => {
123-
setActiveBadge(badgeType);
124-
};
125-
126-
const EmptyStateComponent = () => {
127-
if (articlesToDisplay.length === 0) {
128-
const totalArticlesInAllView = articlesData?.pages[0]?.totalArticle;
129-
if (totalArticlesInAllView === 0) {
130-
return <NoArticles />;
131-
}
132-
return <NoUnreadArticles />;
133-
}
134-
return null;
135-
};
136-
137-
const totalArticleCount =
138-
(category
139-
? categoryArticlesData?.pages[0]?.totalArticle
140-
: articlesData?.pages[0]?.totalArticle) ?? 0;
141-
142-
const totalUnreadArticleCount =
143-
(category
144-
? categoryArticlesData?.pages[0]?.totalUnreadArticle
145-
: articlesData?.pages[0]?.totalUnreadArticle) ?? 0;
146-
14771
return (
14872
<div className="flex h-screen flex-col py-[5.2rem] pl-[8rem] pr-[5rem]">
14973
<div className="flex items-center gap-[0.4rem]">
@@ -162,63 +86,22 @@ const MyBookmark = () => {
16286
<p className="head3 text-main500">{category || ''}</p>
16387
</div>
16488

165-
<div className="mt-[3rem] flex gap-[2.4rem]">
166-
<Badge
167-
text="전체보기"
168-
countNum={totalArticleCount}
169-
onClick={() => handleBadgeClick('all')}
170-
isActive={activeBadge === 'all'}
171-
/>
172-
<Badge
173-
text="안 읽음"
174-
countNum={totalUnreadArticleCount}
175-
onClick={() => handleBadgeClick('notRead')}
176-
isActive={activeBadge === 'notRead'}
177-
/>
178-
</div>
17989
<Tooltip />
18090

181-
{articlesToDisplay.length > 0 ? (
182-
<div
183-
ref={scrollContainerRef}
184-
className="scrollbar-hide mt-[2.6rem] flex h-screen flex-wrap content-start gap-[1.6rem] overflow-y-auto scroll-smooth"
185-
>
186-
{articlesToDisplay.map((article) => (
187-
<FetchCard
188-
key={article.articleId}
189-
article={article}
190-
onClick={() => {
191-
window.open(article.url, '_blank');
192-
updateToReadStatus(article.articleId, {
193-
onSuccess: () => {
194-
// TODO: 쿼리키 팩토리 패턴 적용
195-
queryClient.invalidateQueries({
196-
queryKey: ['bookmarkReadArticles'],
197-
});
198-
queryClient.invalidateQueries({
199-
queryKey: ['bookmarkUnreadArticles'],
200-
});
201-
queryClient.invalidateQueries({
202-
queryKey: ['categoryBookmarkArticles'],
203-
});
204-
queryClient.invalidateQueries({ queryKey: ['arcons'] });
205-
},
206-
onError: (error) => {
207-
console.error(error);
208-
},
209-
});
210-
}}
211-
onOptionsClick={(e) => {
212-
e.stopPropagation();
213-
openMenu(article.articleId, e.currentTarget);
214-
}}
215-
/>
216-
))}
217-
<div ref={observerRef} style={{ height: '1px', width: '100%' }} />
218-
</div>
219-
) : (
220-
<EmptyStateComponent />
221-
)}
91+
<Suspense fallback={<ArticlesLoadingBoundary />}>
92+
<ErrorBoundary FallbackComponent={ArticlesErrorBoundary}>
93+
<MyBookmarkContent
94+
category={category}
95+
categoryId={categoryId}
96+
activeBadge={activeBadge}
97+
onBadgeChange={setActiveBadge}
98+
updateToReadStatus={updateToReadStatus}
99+
openMenu={openMenu}
100+
queryClient={queryClient}
101+
scrollContainerRef={scrollContainerRef}
102+
/>
103+
</ErrorBoundary>
104+
</Suspense>
222105

223106
<OptionsMenuPortal
224107
open={menu.open}
Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,46 @@
1-
import { useInfiniteQuery } from '@tanstack/react-query';
1+
import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
22
import {
33
getBookmarkArticles,
44
getBookmarkUnreadArticles,
55
getCategoryBookmarkArticles,
66
} from './axios';
77

88
export const useGetBookmarkArticles = () => {
9-
return useInfiniteQuery({
9+
return useSuspenseInfiniteQuery({
1010
queryKey: ['bookmarkReadArticles'],
1111
queryFn: ({ pageParam = 0 }) => getBookmarkArticles(pageParam, 20),
1212
initialPageParam: 0,
13-
getNextPageParam: (lastPage, allPages) => {
14-
if (lastPage.articles.length === 0) {
15-
return undefined;
16-
}
17-
return allPages.length;
18-
},
13+
getNextPageParam: (lastPage, allPages) =>
14+
lastPage.articles.length === 0 ? undefined : allPages.length,
1915
});
2016
};
2117

2218
export const useGetBookmarkUnreadArticles = () => {
23-
return useInfiniteQuery({
19+
return useSuspenseInfiniteQuery({
2420
queryKey: ['bookmarkUnreadArticles'],
2521
queryFn: ({ pageParam = 0 }) => getBookmarkUnreadArticles(pageParam, 20),
2622
initialPageParam: 0,
27-
getNextPageParam: (lastPage, allPages) => {
28-
if (lastPage.articles.length === 0) {
29-
return undefined;
30-
}
31-
return allPages.length;
32-
},
23+
getNextPageParam: (lastPage, allPages) =>
24+
lastPage.articles.length === 0 ? undefined : allPages.length,
3325
});
3426
};
3527

3628
export const useGetCategoryBookmarkArticles = (
3729
categoryId: string | null,
3830
readStatus: boolean | null
3931
) => {
40-
return useInfiniteQuery({
32+
return useSuspenseInfiniteQuery({
4133
queryKey: ['categoryBookmarkArticles', readStatus, categoryId],
42-
queryFn: ({ pageParam = 0 }) =>
43-
getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20),
34+
35+
queryFn: ({ pageParam = 0 }) => {
36+
if (!categoryId) return null;
37+
return getCategoryBookmarkArticles(categoryId, readStatus, pageParam, 20);
38+
},
39+
4440
initialPageParam: 0,
4541
getNextPageParam: (lastPage, allPages) => {
46-
if (lastPage.articles.length === 0) {
47-
return undefined;
48-
}
42+
if (!lastPage || lastPage.articles.length === 0) return undefined;
4943
return allPages.length;
5044
},
51-
enabled: !!categoryId,
5245
});
5346
};

0 commit comments

Comments
 (0)