🚀 현재 프로젝트 환경 정리
✅ 네이버 스타일 로그인 & 블로그 시스템 구현 중
✅ 사용된 기술 및 개발 도구 정리
📌 프로젝트 개요
- 프로젝트명: 네이버 스타일 로그인 & 블로그 클론 코딩
- DB: 로컬 스토리지 (LocalStorage) 사용 (별도 백엔드 없이 클라이언트 측 저장)
- 프레임워크: React (JSX 기반 UI 구성)
- 개발 언어: Javascript
- 개발툴: Visual Studio Code
블로그 디자인을 수정하려고 한다.
최근에 적용했던 블로그 디자인은 블로그 들어가자마자 포스트를 작성하는 페이지가 나와,
블로그 같지 않으며 스타일 자체도 어색하여 아래 사진과 같은 디자인으로 수정하려고 한다.
게시글 제목을 클릭하면 상세 화면으로 이동할 수 있도록 만들 것이며, 댓글 기능도 추가하려고 한다.

1. 블로그 디자인 수정

Blog.js
import React, { useState, useEffect } from "react";
import "./Blog.css";
import { useNavigate } from "react-router-dom";
function Blog() {
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
const blogTitle = "네이버 블로그"; // ✅ 블로그 이름 설정
// ✅ 로컬 스토리지에서 기존 글 불러오기
useEffect(() => {
const savedPosts = JSON.parse(localStorage.getItem("posts")) || [];
setPosts(savedPosts);
}, []);
// ✅ 해당 게시글의 댓글 수 가져오기
const getCommentCount = (postId) => {
const comments = JSON.parse(localStorage.getItem(`comments_${postId}`)) || [];
return comments.length;
};
return (
<div className="blog-container">
{/* ✅ 네이버 스타일 블로그 헤더 */}
<header className="blog-header">
<h1 className="blog-title">{blogTitle}</h1>
</header>
{/* ✅ 메인 타이틀 */}
<h2 className="board-title">게시판</h2>
<div className="blog-list">
{posts.length === 0 ? (
<p className="no-posts">작성된 글이 없습니다.</p>
) : (
<div className="post-table">
{posts.map((post) => (
<div
key={post.id}
className="post-row"
onClick={() => navigate(`/post/${post.id}`)}
>
<span className="post-title">{post.title}</span>
<span className="comment-count">{getCommentCount(post.id)}</span>
</div>
))}
</div>
)}
</div>
{/* ✅ 글 작성 버튼 추가 */}
<button className="write-button" onClick={() => navigate("/write")}>
글 작성
</button>
</div>
);
}
export default Blog;
Blog.css
/* ✅ 전체 레이아웃 */
.blog-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* ✅ 네이버 블로그 스타일 헤더 */
.blog-header {
background-color: #04cf5c;
color: white;
padding: 20px;
text-align: center;
font-size: 24px;
font-weight: bold;
}
/* ✅ 메인 게시판 제목 */
.board-title {
font-size: 20px;
font-weight: bold;
margin: 20px 0;
}
/* ✅ 하나의 큰 박스 안에서 밑줄로 구분 */
.post-table {
width: 100%;
border-top: 2px solid #333;
background: white;
padding: 10px;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
}
/* ✅ 게시글 목록 줄 정렬 */
.post-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
font-size: 18px;
cursor: pointer;
border-bottom: 1px solid #ddd;
}
.post-row:hover {
background-color: #f9f9f9;
}
/* ✅ 제목 스타일 */
.post-title {
font-weight: bold;
color: #333;
}
/* ✅ 댓글 개수 스타일 */
.comment-count {
background-color: red;
color: white;
font-size: 14px;
font-weight: bold;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
/* ✅ 글 작성 버튼 */
.write-button {
display: block;
width: 150px;
background-color: #04cf5c;
color: white;
font-size: 18px;
padding: 10px;
border: none;
cursor: pointer;
border-radius: 5px;
text-align: center;
margin-left: auto;
margin-right: 0;
margin-top: 20px;
}
.write-button:hover {
background-color: #03b14b;
}
2. 글쓰기 에디터
현재 React 19.0.0을 사용하기 때문에 Tiptap을 적용했다.
React 버전 18 이하라면 **react-draft-wysiwyg**을 추천한다.
(UI가 직관적이며, 기본 툴바와 기능이 제공되므로 사용이 간편하다.)
반면, Tiptap은 툴바를 직접 추가해야 하며,
이모지, 첨부파일 등의 기능도 별도 라이브러리 설치가 필요하다.
-> 처음엔 react-draft-wysiwyg 의존성 충돌 무시하고 강제 설치하였지만, 나중에 jwt 를 사용하기 위해 axios 를 다운 받을 때 버전 충돌이 일어나서 라이브러리를 삭제해야만 했다.

WritePost.js
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Bold from "@tiptap/extension-bold";
import Italic from "@tiptap/extension-italic";
import Underline from "@tiptap/extension-underline";
import Heading from "@tiptap/extension-heading";
import BulletList from "@tiptap/extension-bullet-list";
import OrderedList from "@tiptap/extension-ordered-list";
import ListItem from "@tiptap/extension-list-item";
import "./Blog.css";
function WritePost() {
const [title, setTitle] = useState("");
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
// ✅ Tiptap Editor 설정 (툴바 포함)
const editor = useEditor({
extensions: [
StarterKit,
Bold,
Italic,
Underline,
Heading,
BulletList,
OrderedList,
ListItem,
],
content: "",
});
useEffect(() => {
const savedPosts = JSON.parse(localStorage.getItem("posts")) || [];
setPosts(savedPosts);
}, []);
const handleSavePost = () => {
if (title.trim() === "" || !editor || editor.getHTML().trim() === "") {
alert("제목과 내용을 입력해주세요.");
return;
}
const newPost = { id: Date.now(), title, content: editor.getHTML() };
const updatedPosts = [newPost, ...posts];
setPosts(updatedPosts);
localStorage.setItem("posts", JSON.stringify(updatedPosts));
setTitle("");
editor.commands.clearContent();
navigate("/"); // ✅ 글 작성 후 메인 화면으로 이동
};
return (
<div className="blog-container">
<h2>네이버 블로그</h2>
<div className="blog-form">
<input
type="text"
placeholder="제목을 입력하세요"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="blog-input"
/>
<h3>글쓰기</h3>
{/* ✅ 툴바 추가 */}
{editor && (
<div className="toolbar">
<button onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive("bold") ? "active" : ""}>
<b>B</b>
</button>
<button onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive("italic") ? "active" : ""}>
<i>I</i>
</button>
<button onClick={() => editor.chain().focus().toggleUnderline().run()} className={editor.isActive("underline") ? "active" : ""}>
<u>U</u>
</button>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} className={editor.isActive("heading", { level: 1 }) ? "active" : ""}>
H1
</button>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={editor.isActive("heading", { level: 2 }) ? "active" : ""}>
H2
</button>
<button onClick={() => editor.chain().focus().toggleBulletList().run()} className={editor.isActive("bulletList") ? "active" : ""}>
• List
</button>
<button onClick={() => editor.chain().focus().toggleOrderedList().run()} className={editor.isActive("orderedList") ? "active" : ""}>
1. List
</button>
<button onClick={() => editor.chain().focus().undo().run()}>↶ Undo</button>
<button onClick={() => editor.chain().focus().redo().run()}>↷ Redo</button>
</div>
)}
<EditorContent editor={editor} className="editor" />
<button onClick={handleSavePost} className="blog-button">글 작성</button>
</div>
</div>
);
}
export default WritePost;
Blog.css
/* 이전 css 미포함 */
/* ✅ 글 작성 제목 */
h2 {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
/* ✅ 글쓰기 폼 */
.blog-form {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
}
/* ✅ 제목 입력 */
.blog-input {
width: 100%;
padding: 12px;
font-size: 18px;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 15px;
}
/* ✅ 툴바 스타일 */
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.toolbar button {
background: #f0f0f0;
border: 1px solid #ddd;
padding: 5px 10px;
cursor: pointer;
border-radius: 3px;
transition: 0.2s ease;
}
.toolbar button.active {
background: #04cf5c;
color: white;
}
/* ✅ Tiptap 에디터 */
.editor {
border: 1px solid #ddd;
min-height: 250px;
padding: 10px;
font-size: 16px;
background: white;
border-radius: 5px;
}
/* ✅ 버튼 정렬 */
.blog-button {
display: block;
width: 100%;
background-color: #04cf5c;
color: white;
font-size: 18px;
padding: 12px;
border: none;
cursor: pointer;
border-radius: 5px;
text-align: center;
margin-top: 15px;
transition: 0.2s ease;
}
.blog-button:hover {
background-color: #03b14b;
}
3. 경고 메시지
삭제, 글 등록, 뒤로가기 및 창 닫기 버튼을 클릭했을 때 안내 메시지를 띄우는 기능을 구현하려고 한다.


WritePost.js
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Bold from "@tiptap/extension-bold";
import Italic from "@tiptap/extension-italic";
import Underline from "@tiptap/extension-underline";
import Heading from "@tiptap/extension-heading";
import BulletList from "@tiptap/extension-bullet-list";
import OrderedList from "@tiptap/extension-ordered-list";
import ListItem from "@tiptap/extension-list-item";
import "./Blog.css";
function WritePost() {
const [title, setTitle] = useState("");
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
// ✅ Tiptap Editor 설정 (툴바 포함)
const editor = useEditor({
extensions: [
StarterKit,
Bold,
Italic,
Underline,
Heading,
BulletList,
OrderedList,
ListItem,
],
content: "",
});
useEffect(() => {
const savedPosts = JSON.parse(localStorage.getItem("posts")) || [];
setPosts(savedPosts);
}, []);
const handleSavePost = () => {
if (!window.confirm("글을 등록하시겠습니까?")) {
return;
}
if (title.trim() === "" || !editor || editor.getHTML().trim() === "") {
alert("제목과 내용을 입력해주세요.");
return;
}
const newPost = { id: Date.now(), title, content: editor.getHTML() };
const updatedPosts = [newPost, ...posts];
setPosts(updatedPosts);
localStorage.setItem("posts", JSON.stringify(updatedPosts));
setTitle("");
editor.commands.clearContent();
navigate("/blog"); // ✅ 글 작성 후 메인 화면으로 이동
};
// ✅ 창 닫기 방지
const usePreventExit = () => {
useEffect(() => {
const handleBeforeUnload = (event) => {
event.preventDefault();
event.returnValue = "";
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, []);
};
// ✅ 뒤로 가기 방지
const usePreventBack = () => {
useEffect(() => {
const handleBack = () => {
if (!window.confirm("글이 저장되지 않을 수 있습니다. 정말 나가시겠습니까?")) {
window.history.pushState(null, "", window.location.href);
} else {
navigate(-1);
}
};
window.history.pushState(null, "", window.location.href);
window.addEventListener("popstate", handleBack);
return () => {
window.removeEventListener("popstate", handleBack);
};
}, [navigate]);
};
// ✅ 훅 적용
usePreventExit();
usePreventBack();
useEffect(() => {
const savedPosts = JSON.parse(localStorage.getItem("posts")) || [];
setPosts(savedPosts);
}, []);
return (
<div className="blog-container">
<h2>네이버 블로그</h2>
<div className="blog-form">
<input
type="text"
placeholder="제목을 입력하세요"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="blog-input"
/>
<h3>글쓰기</h3>
{/* ✅ 툴바 추가 */}
{editor && (
<div className="toolbar">
<button onClick={() => editor.chain().focus().toggleBold().run()} className={editor.isActive("bold") ? "active" : ""}>
<b>B</b>
</button>
<button onClick={() => editor.chain().focus().toggleItalic().run()} className={editor.isActive("italic") ? "active" : ""}>
<i>I</i>
</button>
<button onClick={() => editor.chain().focus().toggleUnderline().run()} className={editor.isActive("underline") ? "active" : ""}>
<u>U</u>
</button>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} className={editor.isActive("heading", { level: 1 }) ? "active" : ""}>
H1
</button>
<button onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={editor.isActive("heading", { level: 2 }) ? "active" : ""}>
H2
</button>
<button onClick={() => editor.chain().focus().toggleBulletList().run()} className={editor.isActive("bulletList") ? "active" : ""}>
• List
</button>
<button onClick={() => editor.chain().focus().toggleOrderedList().run()} className={editor.isActive("orderedList") ? "active" : ""}>
1. List
</button>
<button onClick={() => editor.chain().focus().undo().run()}>↶ Undo</button>
<button onClick={() => editor.chain().focus().redo().run()}>↷ Redo</button>
</div>
)}
<EditorContent editor={editor} className="editor" />
<button onClick={handleSavePost} className="blog-button">글 작성</button>
</div>
</div>
);
}
export default WritePost;
'Project' 카테고리의 다른 글
| [javascript] 네이버 클론 코딩 + 블로그 5: 게시글 수정 (0) | 2025.03.13 |
|---|---|
| [javascript] 네이버 클론 코딩 + 블로그 5: 답글 & 공감 (0) | 2025.03.12 |
| [javascript] 네이버 클론 코딩 + 블로그 4: JWT 발급 (0) | 2025.03.12 |
| [javascript] 네이버 클론 코딩 + 블로그 3: 블로그 댓글 구현 (1) | 2025.03.11 |
| [javascript] 네이버 클론 코딩 + 블로그 1 (0) | 2025.03.06 |