diff --git a/src/components/common/Loading.tsx b/src/components/common/Loading.tsx
new file mode 100644
index 0000000..8bf4ce9
--- /dev/null
+++ b/src/components/common/Loading.tsx
@@ -0,0 +1,25 @@
+import { cn } from '@/lib/utils';
+
+interface LoadingProps {
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ className?: string;
+}
+
+const sizeClasses = {
+ sm: 'w-12 h-12',
+ md: 'w-14 h-14',
+ lg: 'w-16 h-16',
+ xl: 'w-20 h-20',
+};
+
+export function Loading({ size = 'lg', className }: LoadingProps) {
+ return (
+
+ );
+}
diff --git a/src/components/common/LoadingOverlay.tsx b/src/components/common/LoadingOverlay.tsx
new file mode 100644
index 0000000..812abac
--- /dev/null
+++ b/src/components/common/LoadingOverlay.tsx
@@ -0,0 +1,49 @@
+import React, { useEffect, useState } from 'react';
+
+import { useIsFetching, useIsMutating } from '@tanstack/react-query';
+
+import { Loading } from './Loading';
+import { Overlay } from './Overlay';
+
+interface LoadingOverlayProps {
+ className?: string;
+}
+
+export function LoadingOverlay({ className }: LoadingOverlayProps) {
+ const MIN_DELAY = 200;
+
+ const [showLoading, setShowLoading] = useState(false);
+ const isFetching = useIsFetching();
+ const isMutating = useIsMutating();
+
+ const isLoading = isFetching > 0 || isMutating > 0;
+
+ useEffect(() => {
+ let timeout: ReturnType | null = null;
+
+ // 로딩중인 경우 로딩 보여줌
+ if (isLoading) {
+ setShowLoading(true);
+ return;
+ }
+
+ // 로딩 보여주고 있을 때, 최소 0.2초 이상 보여줌
+ if (showLoading) {
+ timeout = setTimeout(() => {
+ setShowLoading(false);
+ }, MIN_DELAY);
+ }
+
+ return () => {
+ if (timeout) clearTimeout(timeout);
+ };
+ }, [isLoading, showLoading]);
+
+ if (!showLoading) return null;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/common/Overlay.tsx b/src/components/common/Overlay.tsx
new file mode 100644
index 0000000..693ae62
--- /dev/null
+++ b/src/components/common/Overlay.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+import { cn } from '@/lib/utils';
+
+interface OverlayProps {
+ className?: string;
+ children?: React.ReactNode;
+}
+
+export function Overlay({ className, children }: OverlayProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index bb5a742..87f1614 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -9,6 +9,7 @@ import { useRouter } from 'next/router';
import ErrorBoundary from '@/components/common/ErrorBoundary';
import Gnb from '@/components/common/Gnb';
+import { LoadingOverlay } from '@/components/common/LoadingOverlay';
import { useInitUser } from '@/hooks/useInitUser';
import type { AppProps } from 'next/app';
@@ -26,7 +27,7 @@ export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const { pathname } = useRouter();
- const pagesWithoutGnb = ['/signup', '/signin', '/oauth/kakao', '/oauth/signup/kakao'];
+ const pagesWithoutGnb = ['/signup', '/signin', '/oauth/signup/kakao'];
const hideHeader = pagesWithoutGnb.includes(pathname);
const isLanding = pathname === '/';
const is404 = pathname === '/404';
@@ -38,6 +39,7 @@ export default function App({ Component, pageProps }: AppProps) {
+
{!hideHeader && }
>} router={router}>