diff --git a/.storybook/preview.ts b/.storybook/preview.ts
index 817ac3ce..23316166 100644
--- a/.storybook/preview.ts
+++ b/.storybook/preview.ts
@@ -1,4 +1,5 @@
import type { Preview } from '@storybook/react';
+import '@/src/styles/global.css';
const preview: Preview = {
parameters: {
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 2d29592a..0256c491 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -2,15 +2,21 @@ import type { AppProps } from 'next/app';
import '@/src/styles/global.css';
+import Layout from '@/src/components/Layout/Layout';
import TanstackQueryProvider from '@/src/components/Providers/TanstackQueryProvider';
import Toast from '@/src/components/Toast/Toast';
+import { pretendard } from '@/src/styles/font';
const App = ({ Component, pageProps }: AppProps) => {
return (
-
-
-
-
+
+
+
+
+
+
+
+
);
};
diff --git a/public/fonts/PretendardVariable.woff2 b/public/fonts/PretendardVariable.woff2
new file mode 100644
index 00000000..49c54b51
Binary files /dev/null and b/public/fonts/PretendardVariable.woff2 differ
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
new file mode 100644
index 00000000..fd3d41df
--- /dev/null
+++ b/src/components/Layout/Layout.tsx
@@ -0,0 +1,7 @@
+import type { PropsWithChildren } from 'react';
+
+const Layout = ({ children }: PropsWithChildren) => {
+ return <>{children}>;
+};
+
+export default Layout;
diff --git a/src/components/Tabs/Tabs.stories.tsx b/src/components/Tabs/Tabs.stories.tsx
new file mode 100644
index 00000000..21cd4ee6
--- /dev/null
+++ b/src/components/Tabs/Tabs.stories.tsx
@@ -0,0 +1,51 @@
+import type { StoryObj } from '@storybook/react';
+import { type Meta } from '@storybook/react';
+
+import Tabs from './Tabs';
+
+const COMPONENT_DESCRIPTION = `
+ - \`\`: 모든 컴포넌트에 대한 컨텍스트와 상태를 제공합니다.
+ - \`\`: \`\` 컴포넌트들을 위한 Wrapper 컴포넌트입니다.
+ - \`\`: 선택된 탭에 해당되는 컨텐츠를 표시하기 위한 버튼입니다.
+ - \`\`: 선택된 탭에 해당되는 컨텐츠를 보여줍니다.
+`;
+
+const meta: Meta = {
+ title: 'Components/Tabs',
+ component: Tabs,
+ tags: ['autodocs'],
+ argTypes: {
+ defaultValue: {
+ description: 'Tabs 컴포넌트의 초기에 활성화될 탭의 기본값',
+ },
+ },
+ parameters: {
+ componentSubtitle: '여러 내용을 한 화면에서 전환하며 볼 수 있는 컴포넌트',
+ docs: {
+ description: {
+ component: COMPONENT_DESCRIPTION,
+ },
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Basic: Story = {
+ render: () => (
+
+
+ 끄적이는
+ 참고하는
+
+
+ 끄적이는 내용
+
+
+ 참고하는 내용
+
+
+ ),
+};
diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx
new file mode 100644
index 00000000..74775106
--- /dev/null
+++ b/src/components/Tabs/Tabs.tsx
@@ -0,0 +1,41 @@
+import { createContext, type PropsWithChildren, useState } from 'react';
+
+import TabsContent from './TabsContent';
+import TabsList from './TabsList';
+import TabsTrigger from './TabsTrigger';
+
+interface TabsContextProps {
+ selectedTab: string;
+ onSelectTab: (selectedTab: string) => void;
+}
+
+export interface TabsRootProps {
+ defaultValue: string;
+}
+
+export const TabsContext = createContext(null);
+
+const TabsRoot = ({
+ children,
+ defaultValue,
+}: PropsWithChildren) => {
+ const [selectedTab, setSelectedTab] = useState(defaultValue);
+
+ const handleTabSelect = (selectedTab: string) => {
+ setSelectedTab(selectedTab);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+const Tabs = Object.assign(TabsRoot, {
+ List: TabsList,
+ Trigger: TabsTrigger,
+ Content: TabsContent,
+});
+
+export default Tabs;
diff --git a/src/components/Tabs/TabsContent.tsx b/src/components/Tabs/TabsContent.tsx
new file mode 100644
index 00000000..36377170
--- /dev/null
+++ b/src/components/Tabs/TabsContent.tsx
@@ -0,0 +1,18 @@
+import type { PropsWithChildren } from 'react';
+
+import { useTabsContext } from '../../hooks/useTabsContext';
+
+interface TabsContentProps {
+ value: string;
+}
+
+const TabsContent = ({
+ children,
+ value,
+}: PropsWithChildren) => {
+ const { selectedTab } = useTabsContext();
+
+ return <>{selectedTab === value ? children : null}>;
+};
+
+export default TabsContent;
diff --git a/src/components/Tabs/TabsList.tsx b/src/components/Tabs/TabsList.tsx
new file mode 100644
index 00000000..0754b24b
--- /dev/null
+++ b/src/components/Tabs/TabsList.tsx
@@ -0,0 +1,9 @@
+import type { PropsWithChildren } from 'react';
+
+import * as styles from './style.css';
+
+const TabsList = ({ children }: PropsWithChildren) => {
+ return ;
+};
+
+export default TabsList;
diff --git a/src/components/Tabs/TabsTrigger.tsx b/src/components/Tabs/TabsTrigger.tsx
new file mode 100644
index 00000000..2bcf4263
--- /dev/null
+++ b/src/components/Tabs/TabsTrigger.tsx
@@ -0,0 +1,28 @@
+import type { PropsWithChildren } from 'react';
+
+import { useTabsContext } from '../../hooks/useTabsContext';
+import * as styles from './style.css';
+
+interface TabsTriggerProps {
+ value: string;
+}
+
+const TabsTrigger = ({
+ children,
+ value,
+}: PropsWithChildren) => {
+ const { selectedTab, onSelectTab } = useTabsContext();
+
+ return (
+
+
+
+ );
+};
+
+export default TabsTrigger;
diff --git a/src/components/Tabs/style.css.ts b/src/components/Tabs/style.css.ts
new file mode 100644
index 00000000..d1fad7c7
--- /dev/null
+++ b/src/components/Tabs/style.css.ts
@@ -0,0 +1,38 @@
+import { style } from '@vanilla-extract/css';
+import { recipe } from '@vanilla-extract/recipes';
+
+import { COLORS } from '@/src/styles/tokens';
+import * as utils from '@/src/styles/utils.css';
+
+export const tabsList = style([
+ utils.flexCenter,
+ {
+ padding: '16px 0 12px',
+ gap: '40px',
+ backgroundColor: COLORS['Grey/White'],
+ },
+]);
+
+export const tabsTrigger = recipe({
+ base: {
+ fontSize: '15px',
+ fontWeight: 500,
+ lineHeight: '18px',
+ letterSpacing: '0px',
+ paddingBottom: '4px',
+ borderBottom: '2px solid transparent',
+ transition: 'all 150ms ease-in-out',
+
+ ':hover': {
+ fontWeight: 700,
+ },
+ },
+ variants: {
+ isActive: {
+ true: {
+ fontWeight: 700,
+ borderBottomColor: '#121212',
+ },
+ },
+ },
+});
diff --git a/src/hooks/useTabsContext.ts b/src/hooks/useTabsContext.ts
new file mode 100644
index 00000000..4bc249f4
--- /dev/null
+++ b/src/hooks/useTabsContext.ts
@@ -0,0 +1,13 @@
+import { useContext } from 'react';
+
+import { TabsContext } from '../components/Tabs/Tabs';
+
+export const useTabsContext = () => {
+ const ctx = useContext(TabsContext);
+
+ if (!ctx) {
+ throw new Error('useTabsContext hook must be used within a Tabs component');
+ }
+
+ return ctx;
+};
diff --git a/src/styles/font.ts b/src/styles/font.ts
new file mode 100644
index 00000000..5458313d
--- /dev/null
+++ b/src/styles/font.ts
@@ -0,0 +1,24 @@
+import localFont from 'next/font/local';
+
+export const pretendard = localFont({
+ src: '../../public/fonts/PretendardVariable.woff2',
+ display: 'swap',
+ preload: true,
+ fallback: [
+ 'Pretendard Variable',
+ 'Pretendard',
+ '-apple-system',
+ 'BlinkMacSystemFont',
+ 'system-ui',
+ 'Roboto',
+ 'Helvetica Neue',
+ 'Segoe UI',
+ 'Apple SD Gothic Neo',
+ 'Noto Sans KR',
+ 'Malgun Gothic',
+ 'Apple Color Emoji',
+ 'Segoe UI Emoji',
+ 'Segoe UI Symbol',
+ 'sans-serif',
+ ],
+});