diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 28da9d37..1d71ce3d 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
+import QueryProvider from '@/context/QueryProvider/QueryProvider';
const inter = Inter({ subsets: ['latin'] });
@@ -16,7 +17,9 @@ export default function RootLayout({
}>) {
return (
-
{children}
+
+ {children}
+
);
}
diff --git a/src/context/AsyncBoundary/AsyncBoundary.tsx b/src/context/AsyncBoundary/AsyncBoundary.tsx
new file mode 100644
index 00000000..9fc5eb77
--- /dev/null
+++ b/src/context/AsyncBoundary/AsyncBoundary.tsx
@@ -0,0 +1,62 @@
+'use client';
+
+import { forwardRef } from 'react';
+import { useQueryErrorResetBoundary } from '@tanstack/react-query';
+import { chain } from '@/util';
+import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';
+import SSRSafeSuspense from '../SSRSafeSuspense/SSRSafeSuspense';
+import type { StrictPropsWithChildren } from '@/types';
+import type { ComponentProps, ComponentRef, Suspense } from 'react';
+
+type ErrorBoundaryProps = Omit<
+ ComponentProps,
+ 'renderFallback'
+>;
+type SuspenseProps = Omit, 'fallback'>;
+
+type AsyncBoundrayProps = StrictPropsWithChildren &
+ ErrorBoundaryProps &
+ SuspenseProps & {
+ errorFallback?: ComponentProps['renderFallback'];
+ pendingFallback?: ComponentProps['fallback'];
+ };
+
+export const AsyncBoundary = forwardRef<
+ ComponentRef,
+ AsyncBoundrayProps
+>(
+ (
+ { errorFallback, pendingFallback, children, ...errorBoundaryProps },
+ ref,
+ ) => {
+ return (
+
+ {children}
+
+ );
+ },
+);
+
+AsyncBoundary.displayName = 'AsyncBoundary';
+
+export const AsyncBoundaryWithQuery = forwardRef<
+ ComponentRef,
+ AsyncBoundrayProps
+>((props, ref) => {
+ const { children, ...otherProps } = props;
+ const { reset } = useQueryErrorResetBoundary();
+
+ return (
+
+ {children}
+
+ );
+});
+
+AsyncBoundaryWithQuery.displayName = 'AsyncBoundaryWithQuery';
diff --git a/src/context/ErrorBoundary/ErrorBoundary.tsx b/src/context/ErrorBoundary/ErrorBoundary.tsx
new file mode 100644
index 00000000..e83771d5
--- /dev/null
+++ b/src/context/ErrorBoundary/ErrorBoundary.tsx
@@ -0,0 +1,154 @@
+'use client';
+
+import {
+ Component,
+ forwardRef,
+ useImperativeHandle,
+ useRef,
+ useState,
+} from 'react';
+import { isDifferentArray } from '@/util';
+import { ErrorboundaryProvider } from './ErrorBoundaryContext';
+import type { StrictPropsWithChildren } from '@/types';
+import type {
+ ComponentPropsWithoutRef,
+ ErrorInfo,
+ PropsWithRef,
+ ReactNode,
+} from 'react';
+
+type RenderFallbackProps = {
+ error: ErrorType;
+ reset?: () => void;
+};
+
+type RenderFallbackType = (
+ props: RenderFallbackProps,
+) => ReactNode;
+
+type FallbackType = ReactNode;
+
+type ErrorBoundaryProps = {
+ onReset?(): void;
+ renderFallback: RenderFallbackType | FallbackType;
+ onError?(error: ErrorType, info: ErrorInfo): void;
+ resetKeys?: unknown[];
+};
+
+interface State {
+ error: ErrorType | null;
+}
+
+const initialState: State = {
+ error: null,
+};
+
+export class ErrorBoundary extends Component<
+ PropsWithRef>,
+ State
+> {
+ hasError = false;
+
+ constructor(
+ props: PropsWithRef>,
+ ) {
+ super(props);
+ this.state = initialState;
+ }
+
+ static getDerivedStateFromError(error: Error) {
+ return { error };
+ }
+
+ componentDidUpdate(prevProps: ErrorBoundaryProps) {
+ const { error } = this.state;
+ const { resetKeys } = this.props;
+
+ if (error === null) {
+ return;
+ }
+ if (!this.hasError) {
+ this.hasError = true;
+ return;
+ }
+
+ if (isDifferentArray(prevProps.resetKeys, resetKeys)) {
+ this.resetErrorBoundary();
+ }
+ }
+
+ componentDidCatch(error: Error, info: ErrorInfo) {
+ const { onError } = this.props;
+
+ onError?.(error, info);
+ }
+
+ resetErrorBoundary = () => {
+ const { onReset } = this.props;
+
+ onReset?.();
+ this.resetState();
+ };
+
+ resetState() {
+ this.hasError = false;
+ this.setState(initialState);
+ }
+
+ genereateRenderedChildren() {
+ const { error } = this.state;
+ const { children, renderFallback } = this.props;
+
+ if (error === null) {
+ return children;
+ }
+
+ return typeof renderFallback === 'function'
+ ? renderFallback({
+ error: error as Error,
+ reset: this.resetErrorBoundary,
+ })
+ : renderFallback;
+ }
+
+ render() {
+ const { error } = this.state;
+
+ const renderedChildren = this.genereateRenderedChildren();
+ const ErrorboundaryProviderProps = {
+ error,
+ resetErrorBoundary: this.resetErrorBoundary,
+ };
+
+ return (
+
+ {renderedChildren}
+
+ );
+ }
+}
+
+export const GlobalErrorBoundary = forwardRef<
+ { reset(): void },
+ ComponentPropsWithoutRef
+>((props, resetRef) => {
+ const ref = useRef(null);
+
+ useImperativeHandle(resetRef, () => ({
+ reset: () => ref.current?.resetErrorBoundary(),
+ }));
+
+ return ;
+});
+
+GlobalErrorBoundary.displayName = 'GlobalErrorBoundary';
+
+export const useErrorBoundary = () => {
+ const [error, setError] = useState(null);
+
+ if (error != null) {
+ throw error as Error;
+ }
+
+ return setError;
+};
diff --git a/src/context/ErrorBoundary/ErrorBoundaryContext.tsx b/src/context/ErrorBoundary/ErrorBoundaryContext.tsx
new file mode 100644
index 00000000..75207e47
--- /dev/null
+++ b/src/context/ErrorBoundary/ErrorBoundaryContext.tsx
@@ -0,0 +1,12 @@
+'use client';
+
+import generateContext from '../generateContext/generateContext';
+
+export const [ErrorboundaryProvider, useErrorBoundaryContext] =
+ generateContext<{
+ error: Error | null;
+ resetErrorBoundary: () => void;
+ }>({
+ name: 'global-error-boundary-context',
+ defaultContextValue: { error: null, resetErrorBoundary: () => {} },
+ });
diff --git a/src/context/Providers.tsx b/src/context/Providers.tsx
deleted file mode 100644
index 513f9151..00000000
--- a/src/context/Providers.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-'use client';
-
-import type { PropsWithChildren } from 'react';
-import ReactQuery from './queryProvider/ReactQuery';
-
-export default function Providers({ children }: PropsWithChildren) {
- return {children};
-}
diff --git a/src/context/QueryProvider/QueryProvider.tsx b/src/context/QueryProvider/QueryProvider.tsx
new file mode 100644
index 00000000..285df395
--- /dev/null
+++ b/src/context/QueryProvider/QueryProvider.tsx
@@ -0,0 +1,8 @@
+'use client';
+
+import ReactQuery from './ReactQuery';
+import type { PropsWithChildren } from 'react';
+
+export default function QueryProvider({ children }: PropsWithChildren) {
+ return {children};
+}
diff --git a/src/context/QueryProvider/ReactQuery.tsx b/src/context/QueryProvider/ReactQuery.tsx
new file mode 100644
index 00000000..b1ddf0e8
--- /dev/null
+++ b/src/context/QueryProvider/ReactQuery.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { QueryClientConfig } from '@tanstack/react-query';
+
+const queryClientOption: QueryClientConfig = {
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ // NOTE: 추후 배포 시 변경
+ staleTime: Infinity,
+ },
+ mutations: {
+ networkMode: 'always',
+ },
+ },
+};
+
+export default function ReactQuery({ children }: PropsWithChildren) {
+ const [queryClient] = useState(() => new QueryClient(queryClientOption));
+
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/src/context/SSRSafeSuspense/SSRSafeSuspense.tsx b/src/context/SSRSafeSuspense/SSRSafeSuspense.tsx
new file mode 100644
index 00000000..85a50070
--- /dev/null
+++ b/src/context/SSRSafeSuspense/SSRSafeSuspense.tsx
@@ -0,0 +1,13 @@
+'use client';
+
+import { useIsClient } from '@/hooks';
+import { Suspense } from 'react';
+import type { ComponentProps } from 'react';
+
+export default function SSRSafeSuspense(props: ComponentProps) {
+ if (!useIsClient()) {
+ // eslint-disable-next-line react/destructuring-assignment
+ return props.fallback;
+ }
+ return ;
+}
diff --git a/src/context/generateContext/generateContext.tsx b/src/context/generateContext/generateContext.tsx
new file mode 100644
index 00000000..8aa03225
--- /dev/null
+++ b/src/context/generateContext/generateContext.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import { createContext, useContext as ReactUseContext, useMemo } from 'react';
+import type { ReactNode, Context as ReactContext } from 'react';
+
+interface ContextOptions {
+ name: string;
+ errorMessage?: string;
+ defaultContextValue?: ContextType;
+ strict?: boolean;
+}
+
+type GenerateContextReturnType = [
+ ({ children, ...providerValues }: ProviderProps) => JSX.Element,
+ () => T,
+ ReactContext,
+];
+
+type ProviderProps =
+ | (ContextValuesType & { children: ReactNode })
+ | { children: ReactNode };
+
+export default function generateContext(
+ options: ContextOptions,
+) {
+ const {
+ name,
+ errorMessage = `${name} Context가 존재하지 않습니다. Provider를 설정해주세요.`,
+ defaultContextValue = null,
+ strict = true,
+ } = options;
+ const Context = createContext(
+ defaultContextValue ?? null,
+ );
+
+ function useContext() {
+ const context = ReactUseContext(Context);
+
+ if (context !== null) {
+ return context;
+ }
+
+ if (defaultContextValue !== null) {
+ return defaultContextValue;
+ }
+
+ if (strict) {
+ const error = new Error(errorMessage);
+ error.name = `${name} ContextError`;
+ Error.captureStackTrace?.(error, useContext);
+ throw error;
+ }
+
+ return context;
+ }
+
+ function Provider({
+ children,
+ ...providerValues
+ }: ProviderProps) {
+ const value = useMemo(
+ () => (Object.keys(providerValues).length > 0 ? providerValues : null),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [...Object.entries(providerValues).flat()],
+ ) as ContextType;
+
+ return {children};
+ }
+
+ Context.displayName = name;
+ Provider.displayName = name;
+
+ return [
+ Provider,
+ useContext,
+ Context,
+ ] as GenerateContextReturnType;
+}
diff --git a/src/context/queryProvider/ReactQuery.tsx b/src/context/queryProvider/ReactQuery.tsx
index 329b2508..b1ddf0e8 100644
--- a/src/context/queryProvider/ReactQuery.tsx
+++ b/src/context/queryProvider/ReactQuery.tsx
@@ -1,10 +1,10 @@
'use client';
-import type { QueryClientConfig } from '@tanstack/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
-import type { PropsWithChildren } from 'react';
import { useState } from 'react';
+import type { PropsWithChildren } from 'react';
+import type { QueryClientConfig } from '@tanstack/react-query';
const queryClientOption: QueryClientConfig = {
defaultOptions: {
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 00000000..46ca4003
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1 @@
+export { default as useIsClient } from './useIsClient';
diff --git a/src/hooks/useIsClient/index.ts b/src/hooks/useIsClient/index.ts
new file mode 100644
index 00000000..7138d384
--- /dev/null
+++ b/src/hooks/useIsClient/index.ts
@@ -0,0 +1,11 @@
+'use client';
+
+import { useSyncExternalStore } from 'react';
+
+export default function useIsClient() {
+ return useSyncExternalStore(
+ () => () => {},
+ () => true,
+ () => false,
+ );
+}
diff --git a/src/system/components/Button/stories/Button.stories.tsx b/src/system/components/Button/stories/Button.stories.tsx
index 53396106..b52be660 100644
--- a/src/system/components/Button/stories/Button.stories.tsx
+++ b/src/system/components/Button/stories/Button.stories.tsx
@@ -1,5 +1,5 @@
-import { Meta } from '@storybook/react';
import { Button } from '../Button';
+import type { Meta } from '@storybook/react';
const meta = {
title: 'Design System/Button',
@@ -9,6 +9,6 @@ const meta = {
export default meta;
-export const Example = () => {
+export function Example() {
return ;
-};
+}
diff --git a/src/system/components/Icon/Icon.tsx b/src/system/components/Icon/Icon.tsx
index 2edabb2d..6e0f1331 100644
--- a/src/system/components/Icon/Icon.tsx
+++ b/src/system/components/Icon/Icon.tsx
@@ -1,12 +1,11 @@
-import { forwardRef } from 'react';
import { Search } from './SVG/Search';
-import { IconBaseType } from '@/system/components/Icon/SVG/type';
import { Folder } from './SVG/Folder';
import { Bell } from './SVG/Bell';
import { Memo } from './SVG/Memo';
import { Profile } from './SVG/Profile';
import { Setting } from './SVG/Setting';
import { Logout } from './SVG/Logout';
+import type { IconBaseType } from '@/system/components/Icon/SVG/type';
const iconMap = {
bell: Bell,
diff --git a/src/system/components/Icon/SVG/Bell.tsx b/src/system/components/Icon/SVG/Bell.tsx
index 886eb5c7..8b218586 100644
--- a/src/system/components/Icon/SVG/Bell.tsx
+++ b/src/system/components/Icon/SVG/Bell.tsx
@@ -1,8 +1,13 @@
-import { IconBaseType } from './type';
+import type { IconBaseType } from './type';
export function Bell({ size, color }: IconBaseType) {
return (
-