
TwitterやThreads、Karriに類似(るいじ)したSNSサービスです。
様々(さまざま)な機能(きのう)を実装(じっそう)した本格的(ほんかくてき)なWebアプリケーションです。
트위터, 쓰레드, 커리어리와 유사한 SNS 서비스입니다.
다양한 기능을 구현한 본격적인 웹 애플리케이션입니다.
구현 예정 기능:
✅ 사용자 관리
- 회원가입 (Sign Up)
- 로그인 / 로그아웃 (Sign In / Out)
- 비밀번호 찾기 (Forget Password)
- 비밀번호 재설정 (Reset Password)
- 사용자 인증 및 인가
✅ 콘텐츠 기능
- 게시글 작성 / 수정 / 삭제
- 이미지 업로드
- 무한 스크롤 (Infinite Scroll)
- 좋아요 (Like)
- 무한 대댓글 (Nested Comments)
✅ UI/UX
- 다크 모드 (Dark Mode)
- 반응형 디자인
- 프로필 페이지
フロントエンド
React 18.3+ (UI 라이브러리)
├─ TypeScript (타입 안전성)
├─ Vite (빌드 도구)
├─ React Router 7 (라우팅)
├─ TanStack Query (서버 상태 관리)
├─ Tailwind CSS (스타일링)
└─ Shadcn/ui + Radix UI (UI 컴포넌트)
バックエンド & インフラ
Supabase (백엔드 서비스)
├─ 인증 (Authentication)
├─ 데이터베이스 (PostgreSQL)
├─ 스토리지 (파일 업로드)
└─ 실시간 구독 (Realtime)
Vercel (배포 플랫폼)
└─ 자동 배포, CDN, 도메인
重要(じゅうよう): 全(すべ)て無料(むりょう)プランを活用(かつよう) - すべて 무료 요금제만 활용 예정
# 프로젝트 클론 또는 생성 후
npm install
# 모든 의존성 설치 완료!
何(なに)がインストールされるか?
주요 의존성:
react: UI 라이브러리
react-router: 페이지 라우팅
@tanstack/react-query: 서버 상태 관리
@supabase/supabase-js: Supabase SDK
tailwindcss: CSS 프레임워크
@radix-ui/*: UI 기본 컴포넌트
lucide-react: 아이콘 라이브러리
npm run dev
# 실행 결과:
# ➜ Local: http://localhost:5173/
# ➜ Network: use --host to expose
ブラウザで確認(かくにん)
브라우저에서 http://localhost:5173 접속
→ 화면이 정상적으로 표시되면 성공!
{
"name": "hanib-log",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite", // 개발 서버 실행
"build": "vite build", // 프로덕션 빌드
"preview": "vite preview" // 빌드 미리보기
},
"dependencies": {
// === React 코어 ===
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^7.1.3",
// === 상태 관리 ===
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-query-devtools": "^5.62.7",
// === UI 컴포넌트 (Radix) ===
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.2.4",
// === 스타일링 ===
"tailwindcss": "^3.4.17",
"tailwind-merge": "^2.6.0",
"clsx": "^2.1.1",
// === 아이콘 ===
"lucide-react": "^0.469.0",
// === 백엔드 ===
"@supabase/supabase-js": "^2.48.1"
},
"devDependencies": {
// TypeScript 관련
"typescript": "~5.6.2",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
// ESLint (코드 품질)
"eslint": "^9.17.0",
// Prettier (코드 포맷팅)
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.9"
}
}
主要(しゅよう)ライブラリ役割(やくわり)
| ライブラリ | 役割(やくわり) | 使用(しよう)目的(もくてき) |
|---|---|---|
@radix-ui/* | UI 基礎(きそ)コンポーネント | アクセシビリティ保証(ほしょう) |
tailwindcss | CSSフレームワーク | 迅速(じんそく)なスタイリング |
@tanstack/react-query | サーバー状態(じょうたい)管理(かんり) | 캐싱, 리페칭 자동화(じどうか) |
完全(かんぜん)な設定(せってい)ファイル
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default tseslint.config(
// === 1. 무시할 폴더 설정 ===
{
ignores: ["dist"] // 빌드 결과물 폴더는 검사 안 함
},
// === 2. 메인 설정 ===
{
// TypeScript 추천 설정 상속
extends: [
js.configs.recommended, // JavaScript 기본 규칙
...tseslint.configs.recommended // TypeScript 기본 규칙
],
// 검사 대상 파일
files: ["**/*.{ts,tsx}"], // 모든 .ts, .tsx 파일
// 언어 옵션
languageOptions: {
ecmaVersion: 2020, // ES2020 문법 사용
globals: globals.browser, // 브라우저 전역 변수 허용
},
// 플러그인 등록
plugins: {
"react-hooks": reactHooks, // React Hooks 규칙
"react-refresh": reactRefresh, // Fast Refresh 규칙
},
// 규칙 설정
rules: {
// React Hooks 규칙 (권장 설정)
...reactHooks.configs.recommended.rules,
// React Refresh 규칙
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
/* === 프로젝트 커스텀 규칙 === */
// 사용하지 않는 변수 에러 끄기
// 이유: 개발 중 임시 변수가 많아서
"@typescript-eslint/no-unused-vars": "off",
// any 타입 사용 허용
// 이유: 빠른 프로토타이핑을 위해
"@typescript-eslint/no-explicit-any": "off",
},
}
);
規則(きそく)の意味(いみ)
✅ 켜진 규칙:
- react-hooks/rules-of-hooks: Hooks 규칙 위반 방지
- react-hooks/exhaustive-deps: useEffect 의존성 체크
- react-refresh/only-export-components: HMR 호환성
❌ 끈 규칙 (실습용):
- no-unused-vars: 사용 안 한 변수 허용
- no-explicit-any: any 타입 허용
실제 프로젝트에서는 이 규칙들을 켜는 것이 좋음!
Path Alias 設定(せってい)追加(ついか)
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* === Bundler mode === */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetectionKind": "force",
"noEmit": true,
"jsx": "react-jsx",
/* === Linting === */
"strict": true,
"noUnusedLocals": false, // 사용 안 한 지역 변수 허용
"noUnusedParameters": false, // 사용 안 한 매개변수 허용
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
/* === 🎯 Path Alias 설정 (추가됨!) === */
"baseUrl": ".", // 기준 경로를 프로젝트 루트로
"paths": {
"@/*": ["./src/*"] // @/ = src/ 폴더
}
},
"include": ["src"]
}
Path Alias 使用例(しようれい)
// ❌ Before: 복잡한 상대 경로
import Button from "../../../components/ui/button";
import { fetchPosts } from "../../../../api/posts";
// ✅ After: 간결한 절대 경로
import Button from "@/components/ui/button";
import { fetchPosts } from "@/api/posts";
장점:
1. 경로가 짧고 명확
2. 파일 이동 시 import 경로 수정 불필요
3. 가독성 향상
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* === Bundler mode === */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetectionKind": "force",
"noEmit": true,
/* === Linting === */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
/* === 🎯 Path Alias 설정 (추가됨!) === */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["vite.config.ts"]
}
Note: Vite 설정(せってい) 파일(ふぁいる)을 위한(ための) TypeScript 설정(せってい)
{
"plugins": ["prettier-plugin-tailwindcss"]
}
Tailwind CSSクラス自動(じどう)整列(せいれつ)
// ❌ Before: 순서 뒤죽박죽
<div className="p-4 text-white bg-blue-500 flex items-center">
// ✅ After: Prettier 자동 정렬
<div className="flex items-center bg-blue-500 p-4 text-white">
정렬 순서:
1. 레이아웃 (flex, grid)
2. 위치 (absolute, relative)
3. 크기 (w-, h-)
4. 배경 (bg-)
5. 간격 (p-, m-)
6. 텍스트 (text-, font-)
...
{
"$schema": "https://ui.shadcn.com/schema.json",
// === 스타일 테마 ===
"style": "new-york", // 'default' 또는 'new-york'
// === React 설정 ===
"rsc": false, // React Server Components 사용 안 함
"tsx": true, // TypeScript + JSX 사용
// === Tailwind 설정 ===
"tailwind": {
"config": "", // tailwind.config 경로 (자동 탐지)
"css": "src/index.css", // 글로벌 CSS 파일
"baseColor": "neutral", // 기본 색상 팔레트
"cssVariables": true, // CSS 변수 사용
"prefix": "" // 클래스 접두사 없음
},
// === Path Alias 설정 ===
"aliases": {
"components": "@/components", // 컴포넌트 폴더
"utils": "@/lib/utils", // 유틸 함수
"ui": "@/components/ui", // UI 컴포넌트
"lib": "@/lib", // 라이브러리
"hooks": "@/hooks" // 커스텀 훅
},
// === 아이콘 라이브러리 ===
"iconLibrary": "lucide" // Lucide React 아이콘 사용
}
Shadcn/ui コンポーネント追加(ついか)
# 버튼 컴포넌트 추가
npx shadcn@latest add button
# 설치되는 것:
# src/components/ui/button.tsx
# → 프로젝트에 복사됨 (npm 패키지 아님!)
# 여러 개 동시 추가
npx shadcn@latest add button input label dialog
# 장점:
# 1. 소스 코드가 프로젝트에 포함됨
# 2. 자유롭게 커스터마이징 가능
# 3. TypeScript 완벽 지원
import { createRoot } from "react-dom/client";
import "./index.css"; // Tailwind 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";
// === 1. TanStack Query 클라이언트 생성 ===
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 기본 옵션 설정
staleTime: 1000 * 60 * 5, // 5분
refetchOnWindowFocus: false, // 윈도우 포커스 시 리페칭 안 함
},
},
});
// === 2. React 앱 렌더링 ===
createRoot(document.getElementById("root")!).render(
// Router Provider (페이지 라우팅)
<BrowserRouter>
{/* TanStack Query Provider (서버 상태 관리) */}
<QueryClientProvider client={queryClient}>
{/* 개발 도구 (캐시 데이터 시각화) */}
<ReactQueryDevtools />
{/* 메인 앱 컴포넌트 */}
<App />
</QueryClientProvider>
</BrowserRouter>
);
提供者(ていきょうしゃ)の順序(じゅんじょ)が重要(じゅうよう)!
올바른 순서:
<BrowserRouter> ← 1. 라우팅 (가장 바깥)
<QueryClientProvider> ← 2. 서버 상태 관리
<ReactQueryDevtools /> ← 3. 개발 도구
<App /> ← 4. 메인 앱
</QueryClientProvider>
</BrowserRouter>
이유:
- BrowserRouter는 전체 앱을 감싸야 함
- QueryClientProvider는 라우트 컴포넌트들이 사용
- ReactQueryDevtools는 쿼리 상태 모니터링
ReactQueryDevtools の役割(やくわり)
개발 도구 기능:
1. 캐시 데이터 시각화
- 모든 쿼리 키와 데이터 표시
- 상태 확인 (fresh, stale, inactive)
2. 실시간 모니터링
- 리페칭 발생 시각화
- 에러 추적
3. 수동 조작
- 캐시 무효화
- 쿼리 재실행
- 캐시 데이터 편집
화면 하단 아이콘으로 토글 가능!
完全(かんぜん)な라우팅(らうてぃんぐ)構造(こうぞう)
import GlobalLayout from "@/components/layout/global-layout";
import ForgetPasswordPage from "@/pages/forget-password-page";
import IndexPage from "@/pages/index-page";
import PostDetailPage from "@/pages/post-detail-page";
import ProfileDetailPage from "@/pages/profile-detail-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";
export default function RootRoute() {
return (
<Routes>
{/* === 글로벌 레이아웃으로 모든 페이지 감싸기 === */}
<Route element={<GlobalLayout />}>
{/* === 인증 관련 페이지 === */}
<Route
path="/sign-in"
element={<SignInPage />}
/>
<Route
path="/sign-up"
element={<SignUpPage />}
/>
<Route
path="/forget-password"
element={<ForgetPasswordPage />}
/>
<Route
path="/reset-password"
element={<ResetPasswordPage />}
/>
{/* === 메인 페이지 === */}
<Route
path="/"
element={<IndexPage />}
/>
{/* === 동적 라우트 (URL 파라미터) === */}
<Route
path="/post/:postId"
element={<PostDetailPage />}
/>
<Route
path="/profile/:userId"
element={<ProfileDetailPage />}
/>
{/* === 404 처리 (모든 잘못된 경로) === */}
<Route
path="*"
element={<Navigate to={"/"} />}
/>
</Route>
</Routes>
);
}
ルート構造(こうぞう)分析(ぶんせき)
페이지 계층 구조:
GlobalLayout (공통 레이아웃)
├─ /sign-in → 로그인 페이지
├─ /sign-up → 회원가입 페이지
├─ /forget-password → 비밀번호 찾기
├─ /reset-password → 비밀번호 재설정
├─ / → 메인 피드 (IndexPage)
├─ /post/:postId → 게시글 상세 (동적)
├─ /profile/:userId → 프로필 페이지 (동적)
└─ /* → 404 → / 리다이렉트
動的(どうてき)ルートの使用(しよう)
// URL 파라미터 사용 예시:
// PostDetailPage.tsx
import { useParams } from "react-router";
export default function PostDetailPage() {
const { postId } = useParams();
// URL: /post/123 → postId = "123"
return <div>게시글 {postId}</div>;
}
// ProfileDetailPage.tsx
export default function ProfileDetailPage() {
const { userId } = useParams();
// URL: /profile/abc → userId = "abc"
return <div>사용자 {userId}의 프로필</div>;
}
Navigate コンポーネント (リダイレクト)
// 잘못된 경로 접근 시:
<Route path="*" element={<Navigate to={"/"} />} />
예시:
/unknown-page → 자동으로 / 이동
/post/abc/def → 자동으로 / 이동
/이상한경로 → 자동으로 / 이동
대신 사용할 수 있는 방법:
<Route path="*" element={<NotFoundPage />} />
→ 404 페이지 직접 표시
完全(かんぜん)な実装(じっそう)
import { Link, Outlet } from "react-router";
import logo from "@/assets/logo.png";
import defaultAvatar from "@/assets/default-avatar.png";
import { SunIcon } from "lucide-react";
export default function GlobalLayout() {
return (
// === 전체 컨테이너: 최소 높이 100vh (뷰포트 전체) ===
<div className="flex min-h-[100vh] flex-col">
{/* ========== 헤더 (Header) ========== */}
<header className="h-15 border-b">
{/* 최대 너비 제한 + 중앙 정렬 컨테이너 */}
<div className="m-auto flex h-full w-full max-w-175 justify-between px-4">
{/* === 왼쪽: 로고 + 제목 === */}
<Link to={"/"} className="flex items-center gap-2">
<img
className="h-5"
src={logo}
alt="한입 로그의 로고, 메세지 말풍선을 형상화한 모양이다"
/>
<div className="font-bold">한입 로그</div>
</Link>
{/* === 오른쪽: 다크 모드 + 프로필 === */}
<div className="flex items-center gap-5">
{/* 다크 모드 토글 버튼 */}
<div className="hover:bg-muted cursor-pointer rounded-full p-2">
<SunIcon />
</div>
{/* 프로필 아바타 */}
<img className="h-6" src={defaultAvatar} />
</div>
</div>
</header>
{/* ========== 메인 콘텐츠 영역 ========== */}
<main className="m-auto w-full max-w-175 flex-1 border-x px-4 py-6">
{/*
Outlet: 자식 라우트 렌더링 위치
예: / → IndexPage
/sign-in → SignInPage
*/}
<Outlet />
</main>
{/* ========== 푸터 (Footer) ========== */}
<footer className="text-muted-foreground border-t py-10 text-center">
@winterlood
</footer>
</div>
);
}
レイアウト構造(こうぞう)分析(ぶんせき)
화면 구조 (Flexbox 세로 배치):
┌──────────────────────────────┐
│ Header (고정 높이) │ ← border-bottom
├──────────────────────────────┤
│ │
│ Main (flex-1: 남은 공간) │ ← Outlet (페이지 내용)
│ │
│ 최대 너비 제한 │
│ 좌우 border │
│ │
├──────────────────────────────┤
│ Footer (자동 높이) │ ← border-top
└──────────────────────────────┘
핵심 CSS:
- min-h-[100vh]: 최소 화면 전체 높이
- flex-col: 세로 배치
- flex-1: main이 남은 공간 모두 차지
ヘッダー詳細(しょうさい)
Header 구성:
┌────────────────────────────────────────┐
│ 🖼️ Logo 한입 로그 ☀️ 👤 │
│ ↑ ↑ ↑ │
│ Link to "/" Dark Profile │
│ Mode │
└────────────────────────────────────────┘
CSS 클래스 설명:
- h-15: 높이 60px (15 × 4px = 60px)
- border-b: 하단 테두리
- max-w-175: 최대 너비 700px (175 × 4px)
- justify-between: 양 끝 배치
- px-4: 좌우 패딩 16px
로고(ろご) + 제목(だいめい) 링크(りんく)
// Link 컴포넌트: 페이지 이동 (새로고침 없음)
<Link to={"/"} className="flex items-center gap-2">
{/* 로고 이미지 */}
<img
className="h-5" // 높이 20px
src={logo} // 이미지 경로
alt="한입 로그의 로고, 메세지 말풍선을 형상화한 모양이다"
// 웹 접근성: 스크린 리더를 위한 설명
/>
{/* 제목 텍스트 */}
<div className="font-bold">한입 로그</div>
</Link>
클릭 시:
/ (메인 페이지)로 이동 → IndexPage 렌더링
ダークモードボタン + アバター
<div className="flex items-center gap-5">
{/* === 다크 모드 토글 === */}
<div className="hover:bg-muted cursor-pointer rounded-full p-2">
<SunIcon />
{/*
Lucide React 아이콘
- SunIcon: 라이트 모드
- MoonIcon: 다크 모드 (추후 전환)
*/}
</div>
{/* === 프로필 아바타 === */}
<img
className="h-6" // 높이 24px
src={defaultAvatar} // 기본 아바타 이미지
alt="사용자 프로필"
/>
</div>
CSS 설명:
- hover:bg-muted: 호버 시 배경색 변경
- cursor-pointer: 마우스 커서 포인터로
- rounded-full: 완전한 원형
- p-2: 패딩 8px (클릭 영역 확대)
メインコンテンツ領域(りょういき)
<main className="m-auto w-full max-w-175 flex-1 border-x px-4 py-6">
<Outlet />
</main>
CSS 분석:
┌─────────────────────────────┐
│ m-auto: 가로 중앙 정렬 │
│ w-full: 너비 100% │
│ max-w-175: 최대 700px │
│ flex-1: 남은 공간 모두 차지 │
│ border-x: 좌우 테두리 │
│ px-4: 좌우 패딩 16px │
│ py-6: 상하 패딩 24px │
└─────────────────────────────┘
Outlet의 역할:
현재 라우트에 해당하는 페이지 컴포넌트 렌더링
예시:
URL: / → <IndexPage />
URL: /sign-in → <SignInPage />
URL: /post/123 → <PostDetailPage />
フッター
<footer className="text-muted-foreground border-t py-10 text-center">
@winterlood
</footer>
CSS:
- text-muted-foreground: 회색 텍스트 (덜 강조)
- border-t: 상단 테두리
- py-10: 상하 패딩 40px
- text-center: 텍스트 중앙 정렬
/* === 레이아웃 === */
flex /* display: flex */
flex-col /* flex-direction: column (세로) */
flex-1 /* flex: 1 1 0% (남은 공간 차지) */
items-center /* align-items: center (세로 중앙) */
justify-between /* justify-content: space-between (양 끝) */
/* === 크기 === */
min-h-[100vh] /* min-height: 100vh (최소 화면 높이) */
h-15 /* height: 60px (15 × 4px) */
h-6 /* height: 24px (6 × 4px) */
h-5 /* height: 20px (5 × 4px) */
w-full /* width: 100% */
max-w-175 /* max-width: 700px (175 × 4px) */
/* === 간격 === */
gap-2 /* gap: 8px (2 × 4px) */
gap-5 /* gap: 20px (5 × 4px) */
px-4 /* padding-left, padding-right: 16px */
py-6 /* padding-top, padding-bottom: 24px */
py-10 /* padding-top, padding-bottom: 40px */
p-2 /* padding: 8px (전체) */
m-auto /* margin: auto (중앙 정렬) */
/* === 테두리 === */
border /* border: 1px solid */
border-b /* border-bottom: 1px solid */
border-t /* border-top: 1px solid */
border-x /* border-left, border-right: 1px solid */
/* === 모양 === */
rounded-full /* border-radius: 9999px (완전한 원) */
/* === 텍스트 === */
font-bold /* font-weight: 700 */
text-center /* text-align: center */
text-muted-foreground /* color: hsl(var(--muted-foreground)) */
/* === 인터랙션 === */
cursor-pointer /* cursor: pointer */
hover:bg-muted /* hover 시 배경색 변경 */
✅ 프로젝트 구조:
React + TypeScript + Vite
TanStack Query (서버 상태)
Supabase (백엔드)
Tailwind + Shadcn/ui (스타일)
✅ 설정 파일:
eslint.config.js: 코드 품질
tsconfig: TypeScript + Path Alias
prettier: Tailwind 자동 정렬
components.json: Shadcn/ui
✅ 라우팅:
React Router 7
중첩 라우트 (Outlet)
동적 파라미터 (:postId, :userId)
404 → / 리다이렉트
✅ 레이아웃:
GlobalLayout: 헤더 + 메인 + 푸터
Flexbox 세로 배치
flex-1로 메인이 남은 공간 차지
최대 너비 제한 (max-w-175)