한입 sns 프로젝트 시작

짱효·2026년 2월 2일

React + TypeScript 프로젝트 초기 설정 및 레이아웃 구현

오늘 작업한 내용을 정리한 문서입니다. 복사하기 편하게 코드와 설명을 포함했습니다.

📋 목차

  1. 페이지 컴포넌트 구조화
  2. Prettier 자동 포맷팅 설정
  3. 전역 레이아웃 및 헤더 구현
  4. 다크모드 설정
  5. Supabase 연동
  6. 배운 점 정리

1. 페이지 컴포넌트 구조화

파일명 및 컴포넌트명 규칙

  • 파일명: kebab-case (예: sign-in-page.tsx)
  • 컴포넌트명: PascalCase + Page (예: 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>
  );
}

2. 전역 레이아웃 및 헤더 구현

GlobalLayout 컴포넌트

// 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">
            &copy; 2026 OneBite Log. All rights reserved. @효윤
          </div>
        </div>
      </footer>
    </div>
  );
}

주요 기능

  1. 헤더

    • 로고와 사이트 이름
    • 다크모드 토글 버튼
    • 사용자 프로필 메뉴 (Popover)
  2. 메인 콘텐츠 영역

    • <Outlet />을 통한 중첩 라우팅
    • 고정 너비 컨테이너
  3. 푸터

    • 저작권 정보

4. 다크모드 설정

ThemeProvider 설정

// 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 다크모드와 연동

5. Supabase 연동

Supabase 클라이언트 설정

// 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

배운 점 정리

1. 파일명 및 컴포넌트명 네이밍 컨벤션

  • 파일명: kebab-case 사용 (sign-in-page.tsx)
  • 컴포넌트명: PascalCase + Page 접미사 (SignInPage)
  • 일관된 네이밍으로 프로젝트 가독성 향상

2. Prettier + Tailwind CSS 플러그인

  • prettier-plugin-tailwindcss로 Tailwind 클래스 자동 정렬
  • 저장 시 자동 포맷팅으로 코드 스타일 통일
  • VSCode 설정으로 개발 경험 개선

3. React Router v7의 중첩 라우팅

  • <Outlet />을 사용한 레이아웃 컴포넌트 패턴
  • useParams로 동적 라우트 파라미터 접근
  • <Navigate />로 404 처리

4. next-themes를 활용한 다크모드

  • ThemeProvider로 전역 테마 관리
  • mounted 상태로 하이드레이션 이슈 방지
  • 시스템 테마 자동 감지

5. Radix UI + shadcn/ui 패턴

  • Popover 컴포넌트로 드롭다운 메뉴 구현
  • 접근성 고려한 컴포넌트 설계
  • asChild prop으로 유연한 컴포넌트 조합

6. TypeScript 타입 안정성

  • Supabase 타입 자동 생성으로 타입 안정성 확보
  • 제네릭을 활용한 타입 추론 (createClient<Database>)

7. 환경 변수 관리

  • .env 파일로 민감한 정보 관리
  • process.env로 환경 변수 접근

8. 개발 도구 통합

  • React Query Devtools로 상태 관리 디버깅
  • ESLint + Prettier로 코드 품질 관리
  • Vite로 빠른 개발 서버 경험

마무리

오늘 작업을 통해 React 프로젝트의 기본 구조를 잡고, 개발 환경을 최적화했습니다. 특히 Prettier 자동 포맷팅과 다크모드 구현이 개발 경험을 크게 향상시켰습니다.

다음 단계로는 각 페이지의 실제 기능 구현과 Supabase를 활용한 인증 및 데이터 관리 기능을 추가할 예정입니다.

profile
✨🌏확장해 나가는 프론트엔드 개발자입니다✏️

0개의 댓글