λΉμ¦μ€μΊ νλ‘ νΈμλ νλ‘μ νΈμ λλ€.
| μΉ΄ν κ³ λ¦¬ | κΈ°μ | λ²μ | μ€λͺ |
|---|---|---|---|
| νλ μμν¬ | React | 19.2.0 | UI λΌμ΄λΈλ¬λ¦¬ |
| μΈμ΄ | TypeScript | 5.9.3 | νμ μμ μ± |
| λ²λ€λ¬ | Vite | 7.2.4 | λΉ λ₯Έ κ°λ° μλ² & λΉλ |
| λΌμ°ν | React Router DOM | 7.12.0 | SPA λΌμ°ν |
| μν κ΄λ¦¬ | Zustand | 5.0.9 | ν΄λΌμ΄μΈνΈ μν κ΄λ¦¬ |
| μλ² μν | TanStack React Query | 5.90.16 | API λ°μ΄ν° μΊμ± & λκΈ°ν |
| νΌ | React Hook Form | 7.70.0 | νΌ μν κ΄λ¦¬ |
| HTTP | Axios | 1.13.2 | API ν΅μ |
| μ€νμΌλ§ | TailwindCSS | 4.1.18 | μ νΈλ¦¬ν° κΈ°λ° CSS |
| μλ¦Ό | Sonner | 2.0.7 | ν μ€νΈ μλ¦Ό |
Note: μ΄ νλ‘μ νΈλ pnpmμ ν¨ν€μ§ λ§€λμ λ‘ μ¬μ©ν©λλ€.
npm install -g pnpmpnpm installνλ‘μ νΈ λ£¨νΈμ .env νμΌμ μμ±νμΈμ:
VITE_API_URL=http://localhost:3000pnpm run devλΈλΌμ°μ μμ http://localhost:5173μΌλ‘ μ μνμΈμ.
| λͺ λ Ήμ΄ | μ€λͺ |
|---|---|
pnpm run dev |
κ°λ° μλ² μ€ν (HMR μ§μ) |
pnpm run build |
νλ‘λμ λΉλ μμ± |
pnpm run preview |
λΉλλ κ²°κ³Όλ¬Ό 미리보기 |
pnpm run lint |
ESLint μ½λ κ²μ¬ |
pnpm run lint:fix |
ESLint μλ μμ |
src/
βββ apis/ # API ν΅μ κ΄λ ¨
β βββ auth/ # μΈμ¦ κ΄λ ¨ API
β β βββ auth.ts # login, signup, logout, getMe, refreshToken ν¨μ
β βββ axiosInstance.ts # Axios μ€μ (μΈν°μ
ν°, ν ν° μλ κ°±μ )
β βββ apiHooks.ts # useAppQuery, useAppMutation 컀μ€ν
ν
β βββ queryClient.ts # React Query ν΄λΌμ΄μΈνΈ μ€μ
β
βββ components/ # μ¬μ¬μ© κ°λ₯ν μ»΄ν¬λνΈ
β βββ Button.tsx # λ²νΌ μ»΄ν¬λνΈ
β βββ HomePage/ # νμ΄μ§λ³ μ»΄ν¬λνΈ ν΄λ
β βββ HomeInput.tsx # Input μ»΄ν¬λνΈ
β
βββ hooks/ # 컀μ€ν
ν
β βββ auth/ # μΈμ¦ κ΄λ ¨ ν
β βββ index.ts # barrel export
β βββ useLogin.ts # λ‘κ·ΈμΈ
β βββ useSignup.ts # νμκ°μ
β βββ useLogout.ts # λ‘κ·Έμμ
β βββ useMe.ts # λ΄ μ 보 μ‘°ν
β βββ useUpdateMe.ts # λ΄ μ 보 μμ
β
βββ lib/ # μ νΈλ¦¬ν° λΌμ΄λΈλ¬λ¦¬
β βββ tokenStorage.ts # ν ν° μ μ₯μ (localStorage/sessionStorage)
β βββ utils.ts # κ³΅ν΅ μ νΈλ¦¬ν° ν¨μ
β
βββ providers/ # React Provider μ»΄ν¬λνΈ
β βββ AuthProvider.tsx # μΈμ¦ μν μ΄κΈ°ν (μ± μμ μ ν ν° κ²μ¦)
β
βββ pages/ # νμ΄μ§ μ»΄ν¬λνΈ
β βββ HomePage.tsx # ννμ΄μ§ (/)
β βββ LoginPage.tsx # λ‘κ·ΈμΈ νμ΄μ§ (/login)
β
βββ layouts/ # λ μ΄μμ μ»΄ν¬λνΈ
β βββ MainLayout.tsx # Header/Footer ν¬ν¨ λ μ΄μμ
β
βββ routes/ # λΌμ°ν
μ€μ
β βββ index.tsx # React Router μ€μ
β
βββ store/ # Zustand μν κ΄λ¦¬
β βββ useAuthStore.ts # μΈμ¦ μν (user, isAuthenticated)
β
βββ types/ # TypeScript νμ
μ μ
β βββ auth.type.ts # μΈμ¦ κ΄λ ¨ νμ
β
βββ styles/ # μ μ μ€νμΌ
β βββ theme.css # CSS λ³μ (μμ, ν°νΈ, κ°κ²© λ±)
β
βββ assets/ # μ μ νμΌ (μ΄λ―Έμ§, μμ΄μ½ λ±)
βββ constants/ # μμ μ μ
β
βββ main.tsx # μ± μ§μ
μ
βββ index.css # μ μ CSS (Tailwind import)
ESLint μ€μ νμΌμ λλ€. λ€μ κ·μΉλ€μ΄ μ μ©λμ΄ μμ΅λλ€:
- Import μλ μ λ ¬:
simple-import-sortνλ¬κ·ΈμΈ μ¬μ©1μμ: react, react-dom 2μμ: @λ‘ μμνλ μΈλΆ ν¨ν€μ§ 3μμ: @/λ‘ μμνλ λ΄λΆ κ²½λ‘ (μ λ κ²½λ‘) 4μμ: ./λ ../λ‘ μμνλ μλ κ²½λ‘ 5μμ: CSS νμΌ - λ―Έμ¬μ© import μλ μ κ±°:
unused-importsνλ¬κ·ΈμΈ - νμ
import κ°μ :
import type { ... }νμ μ¬μ© - Prettier ν΅ν©: μ½λ ν¬λ§·ν κ³Ό λ¦°ν ν΅ν©
{
"semi": true, // μΈλ―Έμ½λ‘ μ¬μ©
"singleQuote": true, // μμλ°μ΄ν μ¬μ©
"tabWidth": 2, // λ€μ¬μ°κΈ° 2μΉΈ
"trailingComma": "es5", // νν μΌν
"printWidth": 100 // ν μ€ μ΅λ 100μ
}- κ²½λ‘ λ³μΉ:
@/λ₯Όsrc/λ‘ λ§€ν// μ¬μ© μμ import { Button } from '@/components/Button'; import { useLogin, useMe } from '@/hooks/auth'; import { tokenStorage } from '@/lib/tokenStorage';
- μ격 λͺ¨λ:
strict: trueλ‘ νμ μμ μ± κ°ν
- React νλ¬κ·ΈμΈ: Fast Refresh μ§μ
- SVGR νλ¬κ·ΈμΈ: SVGλ₯Ό React μ»΄ν¬λνΈλ‘ import κ°λ₯
import { ReactComponent as Logo } from '@/assets/icons/logo.svg'; // λλ import Logo from '@/assets/icons/logo.svg?react';
- TailwindCSS v4: μλ‘μ΄ μμ§ μ¬μ© (
@tailwindcss/postcss) - Autoprefixer: λΈλΌμ°μ νΈνμ± μλ μ²λ¦¬
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β μ± μμ (main.tsx) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AuthProvider β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 1. tokenStorage.get() β μ μ₯λ ν ν° νμΈ β β
β β 2. getMe() β ν ν° μ ν¨μ± κ²μ¦ β β
β β 3. μ€ν¨ μ β refreshToken() β ν ν° κ°±μ μλ β β
β β 4. μ±κ³΅ β useAuthStoreμ user μ 보 μ μ₯ β β
β β 5. μ€ν¨ β logout() μ²λ¦¬ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β isInitialized = false λμ λ‘λ© μ€νΌλ νμ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
v
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RouterProvider β
β (νμ΄μ§ λ λλ§ μμ) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β μ»΄ν¬λνΈ β -> β Custom Hook β -> β API ν¨μ β -> β Axios β
β (LoginPage) β β (useLogin) β β (auth.ts) β β Instance β
βββββββββββββββ βββββββββββββββ βββββββββββββββ βββββββββββββββ
β β
v v
βββββββββββββββ βββββββββββββββ
β React Query β β Interceptor β
β (μΊμ±/μν) β β (ν ν° μ²λ¦¬) β
βββββββββββββββ βββββββββββββββ
μ¬μ© μμ:
// νμ΄μ§μμ ν
μ¬μ©
import { useLogin } from '@/hooks/auth';
const { mutate: login, isPending } = useLogin({ rememberMe: true });
login({ email: 'test@test.com', password: '1234' });apis/axiosInstance.tsμμ λ€μ κΈ°λ₯μ μλ μ²λ¦¬ν©λλ€:
1. μμ² μΈν°μ ν°
λͺ¨λ μμ² β tokenStorageμμ ν ν° μ‘°ν β Authorization ν€λμ μλ μΆκ°
2. μλ΅ μΈν°μ ν° (401 μλ μ²λ¦¬)
API μμ² μ€ν¨ (401 Unauthorized)
β
v
refresh μμ² μμ²΄κ° 401? ββYesββ> λ‘κ·Έμμ β /authλ‘ μ΄λ
β
No
v
μ΄λ―Έ refresh μ€? ββYesββ> λκΈ°μ΄(Queue)μ μΆκ° β refresh μλ£ ν μ¬μλ
β
No
v
refresh ν ν°μΌλ‘ μ access ν ν° λ°κΈ
β
v
μλ μμ² μ¬μλ (μ ν ν° μ¬μ©)
λμ μμ² μ²λ¦¬:
μμ²1 βββ 401 βββ refresh μμ ββββββββ μ±κ³΅! βββ μ¬μλ βββ μλ£
μμ²2 βββ 401 βββ λκΈ°μ΄ μΆκ° ββ λκΈ°μ€... βββ μ¬μλ βββ μλ£
μμ²3 βββ 401 βββ λκΈ°μ΄ μΆκ° ββ λκΈ°μ€... βββ μ¬μλ βββ μλ£
β
processQueue()κ°
λκΈ°μ΄ μ λΆ κΉ¨μ
lib/tokenStorage.tsμμ ν ν°μ κ΄λ¦¬ν©λλ€:
| μ΅μ | μ μ₯μ | μ€λͺ |
|---|---|---|
persist: true |
localStorage | λΈλΌμ°μ λ«μλ μ μ§ (λ‘κ·ΈμΈ μ μ§ O) |
persist: false |
sessionStorage | ν λ«μΌλ©΄ μμ (λ‘κ·ΈμΈ μ μ§ X) |
// λ‘κ·ΈμΈ μ
tokenStorage.set(accessToken, rememberMe); // rememberMe μ²΄ν¬ μ¬λΆμ λ°λΌ μ μ₯μ κ²°μ
// μ‘°ν
tokenStorage.get(); // μλμΌλ‘ μ¬λ°λ₯Έ μ μ₯μμμ μ‘°ν
// λ‘κ·Έμμ μ
tokenStorage.remove(); // λͺ¨λ μ μ₯μμμ μμ store/useAuthStore.tsμμ μΈμ¦ μνλ₯Ό κ΄λ¦¬ν©λλ€:
interface AuthState {
user: User | null; // μ¬μ©μ μ 보
isAuthenticated: boolean; // λ‘κ·ΈμΈ μ¬λΆ
isInitialized: boolean; // μ± μ΄κΈ°ν μλ£ μ¬λΆ
setAuth: (user, token, persist?) => void; // λ‘κ·ΈμΈ μ²λ¦¬
setUser: (user) => void; // μ μ μ 보 μ
λ°μ΄νΈ
logout: () => void; // λ‘κ·Έμμ μ²λ¦¬
}zustand persist μ¬μ© μ μ£Όμ:
// λΆνμν 리λ λλ§ λ°©μ§λ₯Ό μν΄ useShallow μ¬μ© κΆμ₯
import { useShallow } from 'zustand/react/shallow';
const { user, isAuthenticated } = useAuthStore(
useShallow((state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}))
);apis/queryClient.tsμμ κΈ°λ³Έ μ€μ :
staleTime: 5λΆ- λ°μ΄ν°κ° 5λΆκ° fresh μν μ μ§- μλ¬ λ°μ μ Sonner ν μ€νΈλ‘ μλ μλ¦Ό
- React Query Devtools: κ°λ° νκ²½μμ μ°μΈ‘ νλ¨ λ²νΌμΌλ‘ μΊμ μν νμΈ κ°λ₯
styles/theme.cssμ μ μλ CSS λ³μλ€μ
λλ€. Tailwindμ ν¨κ» μ¬μ©νμΈμ.
/* μμ */
--color-primary: #3B82F6; /* λ©μΈ μ»¬λ¬ */
--color-secondary: #6B7280; /* 보쑰 μ»¬λ¬ */
--color-success: #22C55E; /* μ±κ³΅ */
--color-warning: #F59E0B; /* κ²½κ³ */
--color-error: #EF4444; /* μλ¬ */
/* ν°νΈ ν¬κΈ° */
--font-size-xs ~ --font-size-4xl
/* κ°κ²© */
--spacing-1 ~ --spacing-20
/* ν
λ리 λ°κ²½ */
--radius-sm ~ --radius-fullPR μμ± μ μλμΌλ‘ 체ν¬λ¦¬μ€νΈκ° μ 곡λ©λλ€:
- λ²κ·Έ μμ
- ν¬λ‘μ€ λΈλΌμ°μ§ ν μ€νΈ
- λμμΈ/λ§ν¬μ
- κΈ°λ₯ μΆκ°
- 리ν©ν λ§ λ±
λ κ°μ§ μ΄μ νμ μ΄ μ€λΉλμ΄ μμ΅λλ€:
- Feature: μλ‘μ΄ κΈ°λ₯ μμ²
- Fix: λ²κ·Έ 리ν¬νΈ
[νμ
] : μ»€λ° λ©μμ§
| νμ | μ€λͺ |
|---|---|
[Feat] |
μλ‘μ΄ κΈ°λ₯ μΆκ° |
[Fix] |
λ²κ·Έ μμ |
[Refactor] |
μ½λ 리ν©ν λ§ (κΈ°λ₯ λ³κ²½ μμ) |
[Style] |
μ½λ ν¬λ§·ν , μΈλ―Έμ½λ‘ λλ½ λ± |
[Design] |
UI/UX λμμΈ λ³κ²½ |
[Chore] |
λΉλ, μ€μ νμΌ μμ |
[Docs] |
λ¬Έμ μμ |
[Test] |
ν μ€νΈ μ½λ μΆκ°/μμ |
μμ:
[Feat] : λ‘κ·ΈμΈ νμ΄μ§ ꡬν
[Fix] : ν ν° λ§λ£ μ 리λ€μ΄λ νΈ μ€λ₯ μμ
[Refactor] : API νΈμΆ λ‘μ§ λΆλ¦¬
[Chore] : ESLint μ€μ μΆκ°
- μ»΄ν¬λνΈ: PascalCase (
Button.tsx,HomePage.tsx) - ν
: use + camelCase (
useLogin.ts,useAuthQueries.ts) - zustand μ€ν μ΄: use + camelCase + Store (
useAuthStore.ts) - νμ
: camelCase + .type μ λ―Έμ¬ (
auth.type.ts) - μ νΈ: camelCase (
formatDate.ts)
// 1. React
import { useState } from 'react';
// 2. μΈλΆ λΌμ΄λΈλ¬λ¦¬
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
// 3. λ΄λΆ μ λ κ²½λ‘ (@/)
import { Button } from '@/components/Button';
import { useLogin, useMe } from '@/hooks/auth';
import { tokenStorage } from '@/lib/tokenStorage';
import useAuthStore from '@/store/useAuthStore';
// 4. μλ κ²½λ‘
import { HomeInput } from './HomeInput';
// 5. CSS
import './styles.css';// νμ
λ§ importν λλ λ°λμ type ν€μλ μ¬μ©
import type { User, LoginRequest } from '@/types/auth.type';
// κ°κ³Ό νμ
μ ν¨κ» importν λ
import { login, type LoginResponse } from '@/apis/auth/auth';IDEμμ tsconfig.jsonμ λ€μ λ‘λνμΈμ:
- VSCode:
Cmd/Ctrl + Shift + P->TypeScript: Restart TS Server
npm run lint:fixλλ VSCode μ€μ μμ μ μ₯ μ μλ μμ νμ±ν:
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}