[카테고리:] 미분류

  • 시리즈 2/6 — 테마 교체 & 브랜딩

    대상: 웹 프론트엔드 처음 하는 분
    목표: 다크모드 토글, 브랜드 컬러·폰트 적용, 탭 제목/파비콘 교체까지 한 번에.


    0) 사전 상태 점검

    • 프로젝트: Next.js(App Router) + Tailwind v4 + shadcn/ui
    • 포트: 7401
    • 전편에서 만든 파일들이 있다는 가정

    1) 탭 제목(브랜드) 바꾸기

    App Router의 metadata로 설정하는 것이 정석입니다.

    src/app/layout.tsx

    export const metadata = {
      title: "naver.how",
      description: "naver.how admin",
    };
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="ko">
          <body>{children}</body>
        </html>
      );
    }
    

    2) 폰트 적용(한글 + 라틴) — next/font

    로컬 호스팅 + 자동 최적화. 구글폰트 직접 링크 불필요.

    설치 없음. Next 내장 기능이라 바로 사용합니다.

    src/app/layout.tsx

    import { Noto_Sans_KR, Inter } from "next/font/google";
    
    const noto = Noto_Sans_KR({ subsets: ["latin"], weight: ["400","500","700"], variable: "--font-noto" });
    const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
    
    export const metadata = {
      title: "naver.how",
      description: "naver.how admin",
    };
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="ko">
          <body className={`${noto.variable} ${inter.variable} font-sans`}>{children}</body>
        </html>
      );
    }
    

    src/app/globals.css (Tailwind v4)

    @import "tailwindcss";
    @plugin "@tailwindcss/typography";
    
    /* 기본 폰트 스택: Noto Sans KR 우선, 보조로 Inter */
    @layer base {
      :root { color-scheme: light dark; }
      html, body { height: 100%; }
      body { font-family: var(--font-noto), var(--font-inter), system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
    }
    

    3) 브랜드 컬러/그라데이션 토큰 만들기

    v4에서는 CSS 변수를 정의해 두고 Tailwind의 **임의 값(Arbitrary values)**로 바로 씁니다.

    src/app/globals.css 에 아래 블록 추가:

    @layer base {
      :root {
        /* 밝은 테마 */
        --brand: #6d28d9;               /* main (indigo-700 느낌) */
        --brand-2: #f43f5e;             /* accent (rose-500) */
        --surface: #ffffff;
        --text: #0b0b0c;
      }
      .dark {
        /* 다크 테마 */
        --brand: #8b5cf6;               /* main (violet-500) */
        --brand-2: #fb7185;             /* accent (rose-400) */
        --surface: #0b0b0c;
        --text: #e5e7eb;
      }
    
      /* 배경/전경 컬러를 전역으로 */
      body {
        background: var(--surface);
        color: var(--text);
      }
    }
    
    /* 유틸리티(필요한 만큼만) */
    @layer utilities {
      .bg-brand { background-color: var(--brand); }
      .text-brand { color: var(--brand); }
      .bg-brand-2 { background-color: var(--brand-2); }
    }
    

    사용법 예시 (컴포넌트 어디서든):

    <div className="h-7 w-7 rounded-xl bg-[var(--brand)]" />
    <button className="bg-[var(--brand)] text-white hover:opacity-90 px-3 py-2 rounded-xl">
      Primary
    </button>
    

    우리 데모 상단 로고 뱃지의 그라데이션도 바꿔 보세요:

    <div className="h-7 w-7 rounded-xl bg-[var(--brand)] [background:linear-gradient(135deg,var(--brand),var(--brand-2))]" />
    

    4) 다크모드 토글(전역) — 로컬 저장 + OS 연동

    버튼 한번 누르면 .dark 클래스를 html에 달아 전체 색 체계를 전환합니다.

    (A) 전역 Provider 작성

    src/components/theme/ThemeProvider.tsx

    "use client";
    import { createContext, useContext, useEffect, useMemo, useState } from "react";
    
    type Theme = "light" | "dark" | "system";
    type Ctx = { theme: Theme; setTheme: (t: Theme) => void; isDark: boolean };
    
    const ThemeCtx = createContext<Ctx | null>(null);
    
    export default function ThemeProvider({ children }: { children: React.ReactNode }) {
      const [theme, setTheme] = useState<Theme>(() => (typeof window === "undefined" ? "system" : (localStorage.getItem("theme") as Theme) || "system"));
    
      const isDark = useMemo(() => {
        if (theme === "system") {
          if (typeof window === "undefined") return false;
          return window.matchMedia("(prefers-color-scheme: dark)").matches;
        }
        return theme === "dark";
      }, [theme]);
    
      useEffect(() => {
        const root = document.documentElement;
        root.classList.toggle("dark", isDark);
        localStorage.setItem("theme", theme);
      }, [theme, isDark]);
    
      // 시스템 테마 변경 감지
      useEffect(() => {
        if (typeof window === "undefined") return;
        const mq = window.matchMedia("(prefers-color-scheme: dark)");
        const handler = () => { if ((localStorage.getItem("theme") || "system") === "system") {
          document.documentElement.classList.toggle("dark", mq.matches);
        }};
        mq.addEventListener("change", handler);
        return () => mq.removeEventListener("change", handler);
      }, []);
    
      return <ThemeCtx.Provider value={{ theme, setTheme, isDark }}>{children}</ThemeCtx.Provider>;
    }
    
    export const useTheme = () => {
      const ctx = useContext(ThemeCtx);
      if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
      return ctx;
    };
    

    src/app/layout.tsx 에 Provider 감싸기

    import ThemeProvider from "@/components/theme/ThemeProvider";
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="ko">
          <body>
            <ThemeProvider>{children}</ThemeProvider>
          </body>
        </html>
      );
    }
    

    (B) 어디서든 토글 버튼 만들기

    "use client";
    import { useTheme } from "@/components/theme/ThemeProvider";
    import { Moon, Sun, Laptop } from "lucide-react";
    
    export function ThemeToggle(){
      const { theme, setTheme, isDark } = useTheme();
      return (
        <div className="inline-flex items-center gap-2">
          <button onClick={()=>setTheme("light")} className={`px-2 py-1 rounded ${theme==="light"?"bg-gray-200 dark:bg-neutral-800":""}`}>Light</button>
          <button onClick={()=>setTheme("dark")} className={`px-2 py-1 rounded ${theme==="dark"?"bg-gray-200 dark:bg-neutral-800":""}`}>Dark</button>
          <button onClick={()=>setTheme("system")} className={`px-2 py-1 rounded ${theme==="system"?"bg-gray-200 dark:bg-neutral-800":""}`}>System</button>
          <span className="ml-2 text-sm opacity-70 inline-flex items-center gap-1">
            {isDark ? <Moon className="h-4 w-4"/> : <Sun className="h-4 w-4"/>} 현재: {theme}
          </span>
        </div>
      );
    }
    

    우리 데모의 상단바(헤더)에 이 토글을 넣어두면 전역으로 먹습니다.


    5) shadcn/ui와 컬러 변수 연결(간단 버전)

    shadcn 컴포넌트는 bg-primary, text-primary-foreground 같은 클래스를 자주 씁니다.
    Tailwind v4에선 아래처럼 CSS 변수 기반 유틸리티를 만들어주면 바로 적용됩니다.

    src/app/globals.css 에 유틸 추가:

    @layer utilities {
      .bg-primary { background-color: var(--brand); }
      .text-primary-foreground { color: #ffffff; }
      .hover\:bg-primary:hover { background-color: var(--brand); filter: brightness(0.95); }
      .border-border { border-color: rgba(0,0,0,0.12); }
    }
    

    이제 shadcn의 <Button>variant="default"일 때 bg-primary를 쓴다면, 우리가 정의한 –brand 색이 적용됩니다.
    (컴포넌트 소스에서 클래스 확인 후 필요 유틸만 추가하세요.)


    6) 파비콘/로고 교체

    • App Router 규약: src/app/icon.png(또는 .ico, .svg)을 두면 자동 파비콘으로 사용됩니다.
    • 로고 이미지는 public/ 아래에 두고 <Image src="/neverhow_logo.png" ... /> 식으로 사용.
    # 예시
    # 프로젝트 루트에서
    cp ~/Downloads/naverhow_favicon.png src/app/icon.png
    cp ~/Downloads/naverhow_logo.png public/naverhow_logo.png
    

    상단 로고 교체:

    <img src="/naverhow_logo.png" alt="naver.how" className="h-7 w-auto" />
    

    7) 실행 & 확인(포트 7401)

    pnpm approve-builds         # 처음 한 번
    pnpm dev -- -H 0.0.0.0 -p 7401
    

    브라우저 체크:

    • 탭 제목: naverhow
    • 다크/라이트 전환 OK
    • 로고/파비콘 적용 OK
    • 버튼 등 주요 색상에 브랜드 색 반영 OK

    마무리

    이제 **브랜딩 체계(컬러/폰트/타이틀/파비콘/다크모드)**가 갖춰졌어요.
    다음 편에서는 사이드바 그룹 구조(IA) 반영, 라우트 키 설계, 페이지 템플릿(테이블/로그/차트/시트)을 빠르게 찍어내는 법을 다룹니다.