오늘 작업한 내용을 정리한 문서입니다. 복사하기 편하게 코드와 설명을 포함했습니다.
sign-in-page.tsx)SignInPage)// src/pages/sign-in-page.tsx
export default function SignInPage() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold">로그인</h1>
{/* TODO: 로그인 폼 구현 */}
</div>
);
}
// src/pages/sign-up-page.tsx
export default function SignUpPage() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold">회원가입</h1>
{/* TODO: 회원가입 폼 구현 */}
</div>
);
}
// src/pages/forgot-password-page.tsx
export default function ForgotPasswordPage() {
return (
<div className="p-4">
<h1 className="text-2xl font-bold">비밀번호 찾기</h1>
{/* TODO: 비밀번호 찾기 폼 구현 */}
</div>
);
}
// src/root-route.tsx
import ForgotPasswordPage from "@/pages/forgot-password-page";
import HomePage from "@/pages/home-page";
import PostDetailPage from "@/pages/post-detail-page";
import ProfilePage from "@/pages/profile-page";
import ResetPasswordPage from "@/pages/reset-password-page";
import SignInPage from "@/pages/sign-in-page";
import SignUpPage from "@/pages/sign-up-page";
import { Navigate, Route, Routes } from "react-router";
import GlobalLayout from "./components/layout/global-layout";
export default function RootRoute() {
return (
<Routes>
<Route element={<GlobalLayout />}>
<Route path="/sign-in" element={<SignInPage />} />
<Route path="/sign-up" element={<SignUpPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/" element={<HomePage />} />
<Route path="/post/:postId" element={<PostDetailPage />} />
<Route path="/profile/:userId" element={<ProfilePage />} />
<Route path="/reset-password" element={<ResetPasswordPage />} />
<Route path="*" element={<Navigate to={"/"} />} />
</Route>
</Routes>
);
}
// src/components/layout/global-layout.tsx
import { Link, Outlet, useNavigate } from "react-router";
import logo from "@/assets/logo.png";
import defaultAvatar from "@/assets/default-avatar.png";
import { SunIcon, MoonIcon, User, LogOut, Settings } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export default function GlobalLayout() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const navigate = useNavigate();
useEffect(() => {
setMounted(true);
}, []);
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
return (
<div className="bg-background min-h-[100vh]">
<header className="border-border h-15 border-b">
<div className="container mx-auto flex h-15 max-w-175 items-center justify-between px-4">
{/* 로고 영역 */}
<Link
to="/"
className="flex items-center gap-3 transition-opacity hover:opacity-80"
>
<img src={logo} alt="효윤 로그" className="h-8 w-8" />
<div className="text-xl font-bold">효윤 로그</div>
</Link>
{/* 우측 메뉴 영역 */}
<div className="flex items-center gap-3">
{/* 다크모드 토글 버튼 */}
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
className="h-9 w-9"
aria-label="테마 변경"
>
{mounted && theme === "dark" ? (
<SunIcon className="h-5 w-5" />
) : (
<MoonIcon className="h-5 w-5" />
)}
</Button>
{/* 사용자 프로필 메뉴 */}
<Popover>
<PopoverTrigger asChild>
<button className="border-border hover:border-primary focus:ring-ring relative h-9 w-9 overflow-hidden rounded-full border-2 transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-none">
<img
src={defaultAvatar}
alt="사용자 프로필"
className="h-full w-full object-cover"
/>
</button>
</PopoverTrigger>
<PopoverContent className="w-56 p-2" align="end">
<div className="flex flex-col gap-1">
<Button
variant="ghost"
className="w-full justify-start gap-2"
onClick={() => navigate("/profile/me")}
>
<User className="h-4 w-4" />
프로필
</Button>
<Button
variant="ghost"
className="w-full justify-start gap-2"
onClick={() => navigate("/settings")}
>
<Settings className="h-4 w-4" />
설정
</Button>
<div className="bg-border my-1 h-px" />
<Button
variant="ghost"
className="text-destructive hover:text-destructive w-full justify-start gap-2"
onClick={() => navigate("/sign-in")}
>
<LogOut className="h-4 w-4" />
로그아웃
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
</header>
<main className="container mx-auto max-w-175 border-x px-4 py-6">
<Outlet />
</main>
<footer className="border-border h-15 border-t">
<div className="container mx-auto flex h-15 max-w-175 items-center justify-between px-4">
<div className="text-muted-foreground text-sm">
© 2026 OneBite Log. All rights reserved. @효윤
</div>
</div>
</footer>
</div>
);
}
헤더
메인 콘텐츠 영역
<Outlet />을 통한 중첩 라우팅푸터
// src/main.tsx
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { BrowserRouter } from "react-router";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ThemeProvider } from "next-themes";
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools />
<App />
</QueryClientProvider>
</ThemeProvider>
</BrowserRouter>
);
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const toggleTheme = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
// 렌더링 시 mounted 체크 (SSR 하이드레이션 이슈 방지)
{mounted && theme === "dark" ? (
<SunIcon className="h-5 w-5" />
) : (
<MoonIcon className="h-5 w-5" />
)}
핵심 포인트:
mounted 상태로 클라이언트 사이드에서만 렌더링defaultTheme="system"으로 시스템 설정 따름attribute="class"로 Tailwind CSS 다크모드와 연동// src/lib/supabase.ts
import { type Database } from "@/database.types";
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL;
const supabaseKey = process.env.REACT_APP_SUPABASE_PUBLISHABLE_DEFAULT_KEY;
export const supabase = createClient<Database>(supabaseUrl, supabaseKey);
// package.json
{
"scripts": {
"type-gen": "npx supabase gen types typescript --project-id \"nijceiqekadzdcvdpfet\" --schema public > src/database.types.ts"
}
}
사용법:
npm run type-gen
sign-in-page.tsx)SignInPage)prettier-plugin-tailwindcss로 Tailwind 클래스 자동 정렬<Outlet />을 사용한 레이아웃 컴포넌트 패턴useParams로 동적 라우트 파라미터 접근<Navigate />로 404 처리ThemeProvider로 전역 테마 관리mounted 상태로 하이드레이션 이슈 방지Popover 컴포넌트로 드롭다운 메뉴 구현asChild prop으로 유연한 컴포넌트 조합createClient<Database>).env 파일로 민감한 정보 관리process.env로 환경 변수 접근오늘 작업을 통해 React 프로젝트의 기본 구조를 잡고, 개발 환경을 최적화했습니다. 특히 Prettier 자동 포맷팅과 다크모드 구현이 개발 경험을 크게 향상시켰습니다.
다음 단계로는 각 페이지의 실제 기능 구현과 Supabase를 활용한 인증 및 데이터 관리 기능을 추가할 예정입니다.