diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx
deleted file mode 100644
index ad5359c..0000000
--- a/src/AppRouter.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { RouterProvider, createBrowserRouter } from "react-router-dom";
-import App from "@/App";
-import { PATH } from "@/constants/path";
-import ContentDetailPage from "@/pages/ContentDetailPage";
-import HomePage from "@/pages/HomePage";
-import LandingPage from "@/pages/LandingPage";
-import MyPage from "@/pages/MyPage";
-import OnBoardingPage from "@/pages/OnBoardingPage";
-import RedirectPage from "@/pages/RedirectPage";
-import AdminPage from "./pages/AdminPage";
-import SearchPage from "./pages/SearchPage";
-import ShortsPage from "./pages/ShortsPage";
-
-const AppRouter = () => {
- const router = createBrowserRouter([
- {
- path: PATH.ROOT,
- element: ,
- children: [
- {
- path: "",
- element: ,
- },
- {
- path: PATH.HOME,
- element: ,
- },
- {
- path: PATH.CONTENT_DETAIL,
- element: ,
- },
- {
- path: PATH.SHORTS,
- element: ,
- },
- {
- path: PATH.REDIRECT,
- element: ,
- },
- {
- path: PATH.ONBOARDING,
- element: ,
- },
- {
- path: PATH.MYPAGE,
- element: ,
- },
- { path: PATH.SEARCH, element: },
- ],
- },
- {
- path: PATH.ADMIN,
- element: ,
- },
- ]);
- return ;
-};
-export default AppRouter;
diff --git a/src/components/common/ErrorFallback.tsx b/src/components/common/ErrorFallback.tsx
index e56b233..2b9b4d4 100644
--- a/src/components/common/ErrorFallback.tsx
+++ b/src/components/common/ErrorFallback.tsx
@@ -2,43 +2,57 @@ import { useNavigate } from "react-router-dom";
import { HTTPError } from "@/apis/HTTPError";
import { Button } from "@/components/ui/button";
import { HTTP_ERROR_MESSAGES } from "@/constants/api";
-import { PATH } from "@/constants/path";
interface ErrorFallbackProps {
error: Error;
resetErrorBoundary: () => void;
}
+const shouldRetry = (err: Error): boolean => {
+ if (err instanceof HTTPError) {
+ const retryableStatuses = [500, 502, 503];
+ return retryableStatuses.includes(err.status);
+ }
+ return false;
+};
-const ErrorFallback = ({ error, resetErrorBoundary }: ErrorFallbackProps) => {
- const navigate = useNavigate();
+const getMessage = (err: Error): string => {
+ if (err instanceof HTTPError) {
+ const message =
+ HTTP_ERROR_MESSAGES[err.status as keyof typeof HTTP_ERROR_MESSAGES];
- const getMessage = (err: Error): string => {
- if (err instanceof HTTPError) {
- const message =
- HTTP_ERROR_MESSAGES[err.status as keyof typeof HTTP_ERROR_MESSAGES];
+ if (typeof message === "function") {
+ return message(err.status);
+ }
- if (typeof message === "function") {
- return message(err.status);
- }
+ return message ?? HTTP_ERROR_MESSAGES.DEFAULT(err.status);
+ }
- return message ?? HTTP_ERROR_MESSAGES.DEFAULT(err.status);
- }
+ return err.message || HTTP_ERROR_MESSAGES.UNKNOWN;
+};
- return err.message || HTTP_ERROR_MESSAGES.UNKNOWN;
- };
+const ErrorFallback = ({ error, resetErrorBoundary }: ErrorFallbackProps) => {
+ const navigate = useNavigate();
+ const retryable = shouldRetry(error);
return (
문제가 발생했어요
-
{getMessage(error)}
+
+ {getMessage(error)}
+
+
-
+ {retryable && (
+
+ )}
diff --git a/src/constants/api.ts b/src/constants/api.ts
index 3bfa40b..c239a96 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -46,7 +46,7 @@ export const AUTH_ERROR_CODE = {
} as const;
export const HTTP_ERROR_MESSAGES = {
- 404: "찾으시는 콘텐츠가 존재하지 않아요.\n주소가 정확한지 확인해 주세요.",
+ 404: "찾으시는 콘텐츠가 존재하지 않아요.",
500: "서버에 문제가 발생했어요.\n조금 뒤에 다시 시도해 주세요.",
DEFAULT: (status: number) => `문제가 발생했어요. (에러 코드: ${status})`,
UNKNOWN: "알 수 없는 오류가 발생했어요.\n앱을 다시 실행해 보시겠어요?",
diff --git a/src/hooks/queries/shorts/useShortsInfiniteQuery.ts b/src/hooks/queries/shorts/useShortsInfiniteQuery.ts
index 1cb8eb8..ddf4154 100644
--- a/src/hooks/queries/shorts/useShortsInfiniteQuery.ts
+++ b/src/hooks/queries/shorts/useShortsInfiniteQuery.ts
@@ -1,10 +1,10 @@
-import { useInfiniteQuery } from "@tanstack/react-query";
+import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
import { getShorts } from "@/apis/shorts/getShorts";
import type { GetShortsResponse } from "@/types/shorts";
-export const useShortsInfiniteQuery = (options?: { enabled?: boolean }) => {
- const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
- useInfiniteQuery({
+export const useShortsInfiniteQuery = () => {
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
+ useSuspenseInfiniteQuery({
queryKey: ["shorts"],
queryFn: ({ pageParam = null }) =>
getShorts({ cursor: pageParam as number | undefined }),
@@ -12,7 +12,6 @@ export const useShortsInfiniteQuery = (options?: { enabled?: boolean }) => {
lastPage.hasNext ? lastPage.nextCursor : undefined,
initialPageParam: null,
staleTime: 60 * 1000,
- enabled: options?.enabled ?? true,
});
const shorts = data?.pages.flatMap((page) => page.items) ?? [];
@@ -22,6 +21,5 @@ export const useShortsInfiniteQuery = (options?: { enabled?: boolean }) => {
fetchNextPage,
hasNextPage,
isFetchingNextPage,
- isLoading,
};
};
diff --git a/src/hooks/queries/user/useUserInformationQuery.ts b/src/hooks/queries/user/useUserInformationQuery.ts
index e28586a..dcaa2f8 100644
--- a/src/hooks/queries/user/useUserInformationQuery.ts
+++ b/src/hooks/queries/user/useUserInformationQuery.ts
@@ -1,9 +1,9 @@
-import { useQuery } from "@tanstack/react-query";
+import { useSuspenseQuery } from "@tanstack/react-query";
import getUserInformation from "@/apis/user/getUserInformation";
import type { UserInformation } from "@/types/user";
const useUserInformationQuery = () => {
- const { data: userInformation } = useQuery({
+ const { data: userInformation } = useSuspenseQuery({
queryKey: ["userInformation"],
queryFn: () => getUserInformation(),
staleTime: 60 * 60 * 1000,
diff --git a/src/hooks/shorts/useShortsToShow.ts b/src/hooks/shorts/useShortsToShow.ts
index 0f84f19..443257f 100644
--- a/src/hooks/shorts/useShortsToShow.ts
+++ b/src/hooks/shorts/useShortsToShow.ts
@@ -3,10 +3,8 @@ import { useShortsByIdQuery } from "@/hooks/queries/shorts/useShortsByIdQuery";
import { useShortsInfiniteQuery } from "@/hooks/queries/shorts/useShortsInfiniteQuery";
export function useShortsToShow(currentShortsId?: string) {
- const { shorts, fetchNextPage, hasNextPage, isLoading } =
- useShortsInfiniteQuery();
+ const { shorts, fetchNextPage, hasNextPage } = useShortsInfiniteQuery();
- // 타입 안전한 ID 비교 함수
const isSameShortsId = (shortsId: number, targetId: string) => {
return String(shortsId) === targetId && targetId !== "";
};
@@ -24,22 +22,22 @@ export function useShortsToShow(currentShortsId?: string) {
});
const shortsToShow = useMemo(() => {
- // currentShortsId가 없으면 기존 목록 반환
+ // ID 없으면 기존 리스트 반환
if (!currentShortsId) {
return shorts.filter(Boolean);
}
- // 이미 목록에 있으면 기존 목록 반환
+ // 이미 포함되어 있으면 기존 리스트 반환
if (alreadyHasShort) {
return shorts.filter(Boolean);
}
- // 단일 쇼츠가 아직 로딩 중이면 빈 배열 반환 (로딩 완료까지 기다림)
+ // 단일 쇼츠 로딩 중이면 Suspense fallback이 표시될 것이므로, 일단 비워둠
if (isSingleShortsLoading) {
return [];
}
- // 단일 쇼츠를 성공적으로 가져왔으면 맨 앞에 추가
+ // 단일 쇼츠 성공 시, 가장 앞에 추가
if (singleShorts) {
return [
singleShorts,
@@ -47,7 +45,7 @@ export function useShortsToShow(currentShortsId?: string) {
];
}
- // 단일 쇼츠 로딩 실패 시 기존 목록 반환
+ // 실패 fallback: 기존 리스트
return shorts.filter(Boolean);
}, [
currentShortsId,
@@ -61,6 +59,5 @@ export function useShortsToShow(currentShortsId?: string) {
shortsToShow,
fetchNextPage,
hasNextPage,
- isLoading: isLoading || isSingleShortsLoading,
};
}
diff --git a/src/main.tsx b/src/main.tsx
index b0a2d7a..98cc018 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,7 +2,7 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "@/hooks/queries/common/queryClient";
-import AppRouter from "./AppRouter";
+import AppRouter from "./router/AppRouter";
import "./index.css";
createRoot(document.getElementById("root")!).render(
diff --git a/src/pages/MyPage.tsx b/src/pages/MyPage.tsx
index 9d9439d..faad0e0 100644
--- a/src/pages/MyPage.tsx
+++ b/src/pages/MyPage.tsx
@@ -1,12 +1,19 @@
+import AsyncBoundary from "@/components/common/AsyncBoundary";
import MyLayout from "@/components/My/MyLayout";
import useUserInformationQuery from "@/hooks/queries/user/useUserInformationQuery";
-const MyPage = () => {
+const MyPageContent = () => {
const { userInformation } = useUserInformationQuery();
- if (!userInformation) return null;
-
return ;
};
+const MyPage = () => {
+ return (
+
+
+
+ );
+};
+
export default MyPage;
diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx
new file mode 100644
index 0000000..fc1c6c9
--- /dev/null
+++ b/src/pages/NotFoundPage.tsx
@@ -0,0 +1,24 @@
+import { useNavigate } from "react-router-dom";
+import { Button } from "@/components/ui/button";
+
+const NotFoundPage = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+ 페이지를 찾을 수 없습니다
+
+
+ 요청하신 주소가 존재하지 않거나 변경되었습니다.
+
+
+
+ );
+};
+
+export default NotFoundPage;
diff --git a/src/pages/ShortsPage.tsx b/src/pages/ShortsPage.tsx
index 24becb1..c11652c 100644
--- a/src/pages/ShortsPage.tsx
+++ b/src/pages/ShortsPage.tsx
@@ -1,6 +1,8 @@
+/* eslint-disable no-use-before-define */
import { useRef, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query";
+import AsyncBoundary from "@/components/common/AsyncBoundary";
import ShortsLayout from "@/components/shorts/ShortsLayout";
import { useActiveShortsId } from "@/hooks/shorts/useActiveShortsId";
import { useShortsToShow } from "@/hooks/shorts/useShortsToShow";
@@ -9,36 +11,48 @@ import { useShortsWatchTimeTracker } from "@/hooks/shorts/useShortsWatchTimeTrac
const ShortsPage = () => {
const { id: currentShortsId } = useParams<{ id: string }>();
const queryClient = useQueryClient();
- const { shortsToShow, fetchNextPage, hasNextPage, isLoading } =
- useShortsToShow(currentShortsId);
-
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
- // 쇼츠 페이지를 벗어날 때 무한스크롤 캐시 리셋
useEffect(() => {
return () => {
- // cleanup 함수: 컴포넌트가 언마운트될 때 실행
queryClient.resetQueries({ queryKey: ["shorts"] });
};
}, [queryClient]);
+ useShortsWatchTimeTracker({ activeShortsId: currentShortsId });
+
+ return (
+
+
+
+ );
+};
+
+function ShortsContent({
+ cardRefs,
+ currentShortsId,
+}: {
+ cardRefs: React.MutableRefObject<(HTMLDivElement | null)[]>;
+ currentShortsId?: string;
+}) {
+ const { shortsToShow, fetchNextPage, hasNextPage } =
+ useShortsToShow(currentShortsId);
+
useActiveShortsId({
shortsToShow,
cardRefs,
currentShortsId,
});
- useShortsWatchTimeTracker({ activeShortsId: currentShortsId });
-
return (
);
-};
+}
export default ShortsPage;
diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx
new file mode 100644
index 0000000..7d1d4b3
--- /dev/null
+++ b/src/router/AppRouter.tsx
@@ -0,0 +1,100 @@
+import { Suspense } from "react";
+import { RouterProvider, createBrowserRouter } from "react-router-dom";
+import App from "@/App";
+import { PATH } from "@/constants/path";
+import * as Lazy from "./lazy";
+
+const AppRouter = () => {
+ const router = createBrowserRouter([
+ {
+ path: PATH.ROOT,
+ element: ,
+ children: [
+ {
+ path: "",
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: PATH.HOME,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: PATH.CONTENT_DETAIL,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: PATH.SHORTS,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: PATH.REDIRECT,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: PATH.ONBOARDING,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: PATH.MYPAGE,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: PATH.SEARCH,
+ element: (
+
+
+
+ ),
+ },
+ {
+ path: "*",
+ element: (
+
+
+
+ ),
+ },
+ ],
+ },
+ {
+ path: PATH.ADMIN,
+ element: (
+
+
+
+ ),
+ },
+ ]);
+
+ return ;
+};
+
+export default AppRouter;
diff --git a/src/router/lazy.ts b/src/router/lazy.ts
new file mode 100644
index 0000000..c1f5862
--- /dev/null
+++ b/src/router/lazy.ts
@@ -0,0 +1,14 @@
+import { lazy } from "react";
+
+export const LandingPage = lazy(() => import("@/pages/LandingPage"));
+export const HomePage = lazy(() => import("@/pages/HomePage"));
+export const ContentDetailPage = lazy(
+ () => import("@/pages/ContentDetailPage")
+);
+export const ShortsPage = lazy(() => import("@/pages/ShortsPage"));
+export const RedirectPage = lazy(() => import("@/pages/RedirectPage"));
+export const OnBoardingPage = lazy(() => import("@/pages/OnBoardingPage"));
+export const MyPage = lazy(() => import("@/pages/MyPage"));
+export const SearchPage = lazy(() => import("@/pages/SearchPage"));
+export const AdminPage = lazy(() => import("@/pages/AdminPage"));
+export const NotFoundPage = lazy(() => import("@/pages/NotFoundPage"));
diff --git a/tailwind.config.js b/tailwind.config.js
index 6748332..25ac597 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -60,7 +60,7 @@ export default {
},
custom: {
black: "#141516",
- point: "#40FEA4",
+ point: "#2FC88F",
gray: "#A1A1A1",
darkgray: "#4A4949",
},