diff --git a/src/pages/common/ErrorPage/ErrorPage.styles.ts b/src/pages/common/ErrorPage/ErrorPage.styles.ts
new file mode 100644
index 0000000..d1fe6fb
--- /dev/null
+++ b/src/pages/common/ErrorPage/ErrorPage.styles.ts
@@ -0,0 +1,57 @@
+import styled from '@emotion/styled'
+
+import { theme } from '@/shared/styles/theme'
+
+export const Container = styled.div`
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 16px;
+ padding: 40px 20px;
+ text-align: center;
+ background: ${theme.colors.background};
+ color: ${theme.colors.textPrimary};
+`
+
+export const Title = styled.h1`
+ font-size: 28px;
+ font-weight: 700;
+ margin: 0;
+`
+
+export const Description = styled.p`
+ font-size: 16px;
+ color: ${theme.colors.textColor3};
+ margin: 0;
+`
+
+export const ButtonRow = styled.div`
+ display: flex;
+ gap: 12px;
+ margin-top: 8px;
+`
+
+const BaseButton = styled.button`
+ border: 0;
+ border-radius: 10px;
+ padding: 10px 16px;
+ font-size: 14px;
+ cursor: pointer;
+
+ &:focus-visible {
+ outline: 2px solid ${theme.colors.primary2};
+ outline-offset: 2px;
+ }
+`
+
+export const PrimaryButton = styled(BaseButton)`
+ background: ${theme.colors.primary};
+ color: ${theme.colors.white};
+`
+
+export const SecondaryButton = styled(BaseButton)`
+ background: ${theme.colors.lightGray};
+ color: ${theme.colors.textPrimary};
+`
diff --git a/src/pages/common/ErrorPage/ErrorPage.tsx b/src/pages/common/ErrorPage/ErrorPage.tsx
new file mode 100644
index 0000000..4d06488
--- /dev/null
+++ b/src/pages/common/ErrorPage/ErrorPage.tsx
@@ -0,0 +1,25 @@
+import { useNavigate, useRouteError } from 'react-router-dom'
+
+import * as S from './ErrorPage.styles'
+
+export default function ErrorPage() {
+ const navigate = useNavigate()
+ const error = useRouteError() as { status?: number; statusText?: string; message?: string } | null
+
+ const description = error?.statusText || error?.message || '요청하신 페이지를 찾을 수 없어요.'
+
+ return (
+
+ 문제가 발생했어요
+ {description}
+
+ navigate(-1)}>
+ 뒤로가기
+
+ navigate('/')}>
+ 홈으로
+
+
+
+ )
+}
diff --git a/src/routes/Router.tsx b/src/routes/Router.tsx
index ae2b6d9..b1fe29d 100644
--- a/src/routes/Router.tsx
+++ b/src/routes/Router.tsx
@@ -1,6 +1,15 @@
import { createBrowserRouter } from 'react-router-dom'
+import ErrorPage from '@/pages/common/ErrorPage/ErrorPage'
+
import AuthRoutes from './AuthRoutes'
import MainRoutes from './MainRoutes'
-export const router = createBrowserRouter([AuthRoutes, MainRoutes])
+export const router = createBrowserRouter([
+ AuthRoutes,
+ MainRoutes,
+ {
+ path: '*',
+ element: ,
+ },
+])
diff --git a/src/shared/api/axios.ts b/src/shared/api/axios.ts
new file mode 100644
index 0000000..c9d3e8c
--- /dev/null
+++ b/src/shared/api/axios.ts
@@ -0,0 +1,19 @@
+import axios from 'axios'
+
+export const axiosInstance = axios.create({
+ baseURL: import.meta.env.VITE_SERVER_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ withCredentials: true,
+})
+
+//TODO: @yujin5959 나중에 이부분 해주세용~
+axiosInstance.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ return Promise.reject(error)
+ },
+)
+
+export default axiosInstance
diff --git a/src/shared/hooks/customQuery.ts b/src/shared/hooks/customQuery.ts
new file mode 100644
index 0000000..4b0c1a1
--- /dev/null
+++ b/src/shared/hooks/customQuery.ts
@@ -0,0 +1,142 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type {
+ InfiniteData,
+ MutationFunction,
+ QueryFunction,
+ QueryKey,
+ UseInfiniteQueryOptions,
+ UseMutationOptions,
+ UseQueryOptions,
+ UseSuspenseQueryOptions,
+} from '@tanstack/react-query'
+import { useInfiniteQuery, useMutation, useQuery, useSuspenseQuery } from '@tanstack/react-query'
+
+type DefaultQueryOptions = {
+ staleTime?: number
+ retry?: number
+}
+
+const STALE_TIME = 1000 * 60 * 3
+const RETRY = 1
+const MUTATION_RETRY = 1
+
+// 기본적인 쿼리 흐름을 정리한 커스텀 훅들:
+// 1) useCustomQuery: 일반 `useQuery` 래퍼로, 스테일 시간·재시도 횟수·포커스 시 리패치 제한을 기본값으로 잡아줍니다.
+// 최초 키/펑션 이외 값을 옵션에 넘겨도 기본 설정을 덮어쓰지 않고 안전하게 적용합니다.
+// 2) useCustomInfiniteQuery: 페이지 단위 API를 위한 `useInfiniteQuery` 래퍼로, 위와 동일한 기본값에 더해 `keepPreviousData` 없이 연속 페이지를 유지합니다.
+
+export const useCustomQuery = <
+ TQueryFnData,
+ TError,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ queryKey: TQueryKey,
+ queryFn: QueryFunction,
+ options?: Omit, 'queryKey' | 'queryFn'> &
+ DefaultQueryOptions,
+) => {
+ const safeOptions =
+ options ??
+ ({} as Omit, 'queryKey' | 'queryFn'> &
+ DefaultQueryOptions)
+
+ const {
+ staleTime,
+ retry,
+ // refetchOnWindowFocus: _refetchOnWindowFocus, // 제거: 사용되지 않음
+ ...restOptions
+ } = safeOptions
+
+ return useQuery({
+ queryKey,
+ queryFn,
+ staleTime: staleTime ?? STALE_TIME,
+ retry: retry ?? RETRY,
+ refetchOnWindowFocus: false,
+ ...restOptions,
+ })
+}
+
+export const useCustomSuspenseQuery = <
+ TQueryFnData,
+ TError extends Error = Error,
+ TData = TQueryFnData,
+ TQueryKey extends QueryKey = QueryKey,
+>(
+ queryKey: TQueryKey,
+ queryFn: QueryFunction,
+ options?: Omit<
+ UseSuspenseQueryOptions,
+ 'queryKey' | 'queryFn'
+ > &
+ DefaultQueryOptions,
+) => {
+ const safeOptions =
+ options ??
+ ({} as Omit<
+ UseSuspenseQueryOptions,
+ 'queryKey' | 'queryFn'
+ > &
+ DefaultQueryOptions)
+
+ const {
+ staleTime,
+ retry,
+ // refetchOnWindowFocus: _refetchOnWindowFocus, // 제거: 사용되지 않음
+ ...restOptions
+ } = safeOptions
+
+ return useSuspenseQuery({
+ queryKey,
+ queryFn,
+ staleTime: staleTime ?? STALE_TIME,
+ retry: retry ?? RETRY,
+ refetchOnWindowFocus: false,
+ ...restOptions,
+ })
+}
+
+export const useCustomInfiniteQuery = <
+ TQueryFnData,
+ TError,
+ TData = InfiniteData,
+ TQueryKey extends QueryKey = QueryKey,
+ TPageParam = unknown,
+>(
+ queryKey: TQueryKey,
+ queryFn: QueryFunction,
+ infiniteOptions?: UseInfiniteQueryOptions &
+ DefaultQueryOptions,
+) => {
+ const safeOptions =
+ infiniteOptions ??
+ ({} as UseInfiniteQueryOptions &
+ DefaultQueryOptions)
+
+ const { staleTime, retry, queryKey: _queryKey, queryFn: _queryFn, ...restOptions } = safeOptions
+
+ return useInfiniteQuery({
+ queryKey,
+ queryFn,
+ staleTime: staleTime ?? STALE_TIME,
+ retry: retry ?? RETRY,
+ refetchOnWindowFocus: false,
+ ...restOptions,
+ })
+}
+
+export const useCustomMutation = (
+ mutationFn: MutationFunction,
+ options?: UseMutationOptions & DefaultQueryOptions,
+) => {
+ const safeOptions =
+ options ?? ({} as UseMutationOptions & DefaultQueryOptions)
+ const { retry, ...restOptions } = safeOptions
+
+ return useMutation({
+ mutationFn,
+ retry: retry ?? MUTATION_RETRY,
+ ...restOptions,
+ })
+}
diff --git a/src/shared/ui/common/AsyncBoundary/AsyncBoundary.tsx b/src/shared/ui/common/AsyncBoundary/AsyncBoundary.tsx
new file mode 100644
index 0000000..1efd9e1
--- /dev/null
+++ b/src/shared/ui/common/AsyncBoundary/AsyncBoundary.tsx
@@ -0,0 +1,78 @@
+import type { ErrorInfo, ReactNode } from 'react'
+import { Component, Suspense } from 'react'
+import { useLocation } from 'react-router-dom'
+
+import AsyncBoundaryFallback from './AsyncBoundaryFallback'
+
+type AsyncBoundaryProps = {
+ children: ReactNode
+ fallback: ReactNode
+ errorFallback?: (error: Error, reset: () => void) => ReactNode
+ resetKeys?: Array
+}
+
+type AsyncBoundaryState = {
+ error?: Error
+}
+
+class ErrorBoundary extends Component<
+ {
+ children: ReactNode
+ fallback?: (error: Error, reset: () => void) => ReactNode
+ resetKeys?: Array
+ },
+ AsyncBoundaryState
+> {
+ state: AsyncBoundaryState = {}
+
+ static getDerivedStateFromError(error: Error) {
+ return { error }
+ }
+
+ componentDidCatch(error: Error, info: ErrorInfo) {
+ console.error(error, info)
+ }
+
+ componentDidUpdate(prevProps: Readonly<{ resetKeys?: Array }>) {
+ const { error } = this.state
+ const { resetKeys } = this.props
+ if (!error || !resetKeys || !prevProps.resetKeys) return
+
+ const hasResetKeyChanged =
+ resetKeys.length !== prevProps.resetKeys.length ||
+ resetKeys.some((key, index) => !Object.is(key, prevProps.resetKeys?.[index]))
+
+ if (hasResetKeyChanged) {
+ this.resetErrorBoundary()
+ }
+ }
+
+ resetErrorBoundary = () => {
+ this.setState({ error: undefined })
+ }
+
+ render() {
+ const { error } = this.state
+ if (error) {
+ const { fallback } = this.props
+ if (fallback) {
+ return fallback(error, this.resetErrorBoundary)
+ }
+ return
+ }
+ return this.props.children
+ }
+}
+
+const AsyncBoundary = ({ children, fallback, errorFallback, resetKeys }: AsyncBoundaryProps) => {
+ const location = useLocation()
+ const derivedResetKeys = resetKeys ?? [location.pathname, location.search, location.hash]
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default AsyncBoundary
diff --git a/src/shared/ui/common/AsyncBoundary/AsyncBoundaryFallback.styles.ts b/src/shared/ui/common/AsyncBoundary/AsyncBoundaryFallback.styles.ts
new file mode 100644
index 0000000..11084c5
--- /dev/null
+++ b/src/shared/ui/common/AsyncBoundary/AsyncBoundaryFallback.styles.ts
@@ -0,0 +1,53 @@
+import styled from '@emotion/styled'
+
+import { theme } from '@/shared/styles/theme'
+
+export const Container = styled.div`
+ min-height: 240px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ padding: 24px;
+ border-radius: 16px;
+ background: ${theme.colors.white};
+ color: ${theme.colors.textPrimary};
+ box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.08);
+ text-align: center;
+`
+
+export const Title = styled.h2`
+ margin: 0;
+ font-size: 20px;
+ font-weight: 700;
+`
+
+export const Description = styled.p`
+ margin: 0;
+ color: ${theme.colors.textColor3};
+ font-size: 14px;
+`
+
+export const ErrorMessage = styled.p`
+ margin: 0;
+ color: ${theme.colors.textColor2};
+ font-size: 12px;
+ opacity: 0.8;
+`
+
+export const Button = styled.button`
+ margin-top: 6px;
+ border: 0;
+ border-radius: 10px;
+ padding: 10px 16px;
+ font-size: 14px;
+ cursor: pointer;
+ background: ${theme.colors.primary};
+ color: ${theme.colors.white};
+
+ &:focus-visible {
+ outline: 2px solid ${theme.colors.primary};
+ outline-offset: 2px;
+ }
+`
diff --git a/src/shared/ui/common/AsyncBoundary/AsyncBoundaryFallback.tsx b/src/shared/ui/common/AsyncBoundary/AsyncBoundaryFallback.tsx
new file mode 100644
index 0000000..7bb771d
--- /dev/null
+++ b/src/shared/ui/common/AsyncBoundary/AsyncBoundaryFallback.tsx
@@ -0,0 +1,28 @@
+import type { ReactNode } from 'react'
+
+import * as S from './AsyncBoundaryFallback.styles'
+
+type AsyncBoundaryFallbackProps = {
+ error: Error
+ onReset: () => void
+ title?: ReactNode
+ description?: ReactNode
+}
+
+const AsyncBoundaryFallback = ({
+ error,
+ onReset,
+ title = '문제가 발생했어요',
+ description,
+}: AsyncBoundaryFallbackProps) => (
+
+ {title}
+ {description ?? '요청을 처리하는 중 오류가 발생했어요.'}
+ {error.message}
+
+ 다시 시도
+
+
+)
+
+export default AsyncBoundaryFallback
diff --git a/src/shared/ui/common/AsyncBoundary/index.ts b/src/shared/ui/common/AsyncBoundary/index.ts
new file mode 100644
index 0000000..a2e4e24
--- /dev/null
+++ b/src/shared/ui/common/AsyncBoundary/index.ts
@@ -0,0 +1,2 @@
+export { default as AsyncBoundary } from './AsyncBoundary'
+export { default as AsyncBoundaryFallback } from './AsyncBoundaryFallback'
diff --git a/src/shared/ui/common/SuspenseFallback/SuspenseFallback.tsx b/src/shared/ui/common/SuspenseFallback/SuspenseFallback.tsx
new file mode 100644
index 0000000..98c57f4
--- /dev/null
+++ b/src/shared/ui/common/SuspenseFallback/SuspenseFallback.tsx
@@ -0,0 +1,47 @@
+import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'
+import { useMemo } from 'react'
+
+type SuspenseFallbackProps = {
+ label?: string
+ gap?: number
+ children?: ReactNode
+} & HTMLAttributes
+
+//TODO: 스타일 추가 필요
+const SuspenseFallback = ({
+ label = '로딩 중입니다',
+ gap = 8,
+ children,
+ style,
+ ...rest
+}: SuspenseFallbackProps) => {
+ const combinedStyle: CSSProperties = useMemo(
+ () => ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap,
+ ...style,
+ }),
+ [gap, style],
+ )
+
+ const spinnerStyle: CSSProperties = {
+ width: 16,
+ height: 16,
+ borderRadius: '50%',
+ border: '2px solid #ccc',
+ borderTopColor: '#555',
+ }
+
+ return (
+
+ )
+}
+
+export default SuspenseFallback
diff --git a/src/shared/ui/common/SuspenseFallback/index.ts b/src/shared/ui/common/SuspenseFallback/index.ts
new file mode 100644
index 0000000..aa441e8
--- /dev/null
+++ b/src/shared/ui/common/SuspenseFallback/index.ts
@@ -0,0 +1 @@
+export { default } from './SuspenseFallback'