Tailwind CSS 설치 (ver 4.1)
https://tailwindcss.com/docs/installation/using-vite
npm install tailwindcss @tailwindcss/vite
vite.config.js 수정
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})
App.css 에 추가
@import "tailwindcss";
레이아웃 컴포넌트 생성
src/
└── layout/
└── MainLayout.jsx
MainLayout.jsx
// src/layout/MainLayout.jsx
import Header from '../components/Header';
function MainLayout({ children }) {
return (
<div className="max-w-3xl mx-auto p-4">
<Header />
<main>{children}</main>
<footer className="text-center mt-10 text-gray-500 text-sm">
ⓒ 2025 My Blog
</footer>
</div>
);
}
export default MainLayout;
Router에서 공통 레이아웃 적용
Router.jsx 수정
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import HomePage from '../pages/HomePage';
import WritePage from '../pages/WritePage';
import PostPage from '../pages/PostPage';
import MainLayout from '../layout/MainLayout';
function Router() {
return (
<BrowserRouter>
<MainLayout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/write" element={<WritePage />} />
<Route path="/post/:id" element={<PostPage />} />
</Routes>
</MainLayout>
</BrowserRouter>
);
}
Header.jsx 개선
import { Link } from 'react-router-dom';
function Header() {
return (
<header className="mb-6 border-b pb-4">
<h1 className="text-2xl font-bold mb-2">
<Link to="/">📝 My Blog</Link>
</h1>
<nav className="space-x-4">
<Link to="/">홈</Link>
<Link to="/write">글쓰기</Link>
</nav>
</header>
);
}
export default Header;
HomePage.jsx 개선
// src/pages/HomePage.jsx
import { useContext } from "react";
import { PostContext } from "../context/PostContext";
import { Link } from "react-router-dom";
function HomePage() {
const { posts } = useContext(PostContext);
return (
<div className="container mx-auto p-4">
<h2>글 목록</h2>
<ul>
{posts.map((post) => (
<li key={post.id} className="border-b py-2">
<Link
to={`/post/${post.id}`}
className="text-lg font-semibold hover:underline"
>
{post.title}
</Link>
</li>
))}
</ul>
</div>
);
}
export default HomePage;
PostForm.jsx 개선
// src/components/PostForm.jsx
import { useState } from 'react';
function PostForm({ initialTitle = '', initialContent = '', onSubmit, submitLabel = "등록" }) {
const [title, setTitle] = useState(initialTitle);
const [content, setContent] = useState(initialContent);
const handleSubmit = e => {
e.preventDefault();
if (!title.trim() || !content.trim()) {
alert("제목과 내용을 모두 입력하세요.");
return;
}
onSubmit(title, content);
};
return (
<form onSubmit={handleSubmit}>
<input className="border border-gray-300 p-2 mb-2 w-full"
placeholder="제목"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<br />
<textarea className="border border-gray-300 p-2 mb-2 w-full h-40"
placeholder="내용"
value={content}
onChange={e => setContent(e.target.value)}
/>
<br />
<button className="bg-blue-500 text-white p-2 rounded-md" type="submit">{submitLabel}</button>
</form>
);
}
export default PostForm;
PostPage.jsx 개선
import { useContext, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { PostContext } from "../context/PostContext";
import PostForm from "../components/PostForm";
function PostPage() {
const { posts, updatePost } = useContext(PostContext);
const { id } = useParams();
const post = posts.find((p) => p.id === Number(id));
const navigate = useNavigate();
const [editing, setEditing] = useState(false);
// 댓글 관련
const [comments, setComments] = useState([]);
const [commentText, setCommentText] = useState("");
const handleCommentSubmit = (e) => {
e.preventDefault();
if (!commentText.trim()) return;
const newComment = { id: Date.now(), text: commentText };
setComments([...comments, newComment]);
setCommentText("");
};
if (!post) return <p>글을 찾을 수 없습니다.</p>;
return (
<div>
{editing ? (
<PostForm
initialTitle={post.title}
initialContent={post.content}
submitLabel="수정"
onSubmit={(title, content) => {
updatePost(post.id, title, content);
setEditing(false);
}}
/>
) : (
<>
<h2 className="text-2xl font-bold mb-2">{post.title}</h2>
<p className="mb-4 text-gray-700">{post.content}</p>
<button className="bg-blue-500 text-white p-2 rounded-md mr-2" onClick={() => setEditing(true)}>수정하기</button>
<button className="bg-red-500 text-white p-2 rounded-md" onClick={() => navigate(-1)}>뒤로가기</button>
</>
)}
<hr />
<h3>댓글</h3>
<form onSubmit={handleCommentSubmit}>
<input className="border border-gray-300 p-2 mb-2 w-full"
type="text"
placeholder="댓글을 입력하세요"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
/>
<button className="bg-blue-500 text-white p-2 rounded-md" type="submit">등록</button>
</form>
<ul>
{comments.map((c) => (
<li className="border-b py-2" key={c.id}>{c.text}</li>
))}
</ul>
</div>
);
}
export default PostPage;반응형
'프로그래밍 > React' 카테고리의 다른 글
| React로 블로그 만들기 - 댓글 전역 상태 관리 (CommentContext) (10) (0) | 2025.07.06 |
|---|---|
| React로 블로그 만들기 - 글 삭제 기능 추가하기 (9) (0) | 2025.07.06 |
| React로 블로그 만들기 - json-server 사용 (REST API mock) (7) (0) | 2025.07.06 |
| React로 블로그 만들기 - 데이터 저장 유지 – localStorage (6) (0) | 2025.07.06 |
| React로 블로그 만들기 - 폼 컴포넌트 분리 및 재사용 (5) (0) | 2025.07.06 |