MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!
프로젝트 소개 및 목표
🔗 관련 에러 해결 가이드
MongoDB와 Express.js로 블로그 만들기 프로젝트는 실무에서 가장 많이 사용되는 Node.js 기반 풀스택 개발 스킬을 익힐 수 있는 최고의 학습 방법입니다. 이 가이드를 통해 RESTful API 설계, NoSQL 데이터베이스 연동, 사용자 인증, CRUD 기능 구현 등 실전 개발 역량을 습득할 수 있습니다. 완성된 블로그는 포트폴리오로 활용할 수 있으며, 추가 기능을 확장하여 더욱 발전시킬 수 있습니다. 개발 입문자부터 중급자까지 누구나 따라할 수 있도록 단계별로 상세하게 설명합니다.
필요한 기술 스택
이 프로젝트를 진행하기 위해 필요한 핵심 기술 스택은 다음과 같습니다:
- Node.js (v14 이상): 서버 사이드 JavaScript 런타임
- Express.js (v4.x): 빠르고 유연한 웹 애플리케이션 프레임워크
- MongoDB (v4.x 이상): NoSQL 데이터베이스
- Mongoose: MongoDB ODM(Object Data Modeling) 라이브러리
- EJS 또는 Handlebars: 템플릿 엔진
- Passport.js: 사용자 인증 미들웨어
- bcrypt: 비밀번호 암호화
프로젝트 셋업
먼저 개발 환경을 구성하겠습니다. Node.js와 MongoDB가 설치되어 있어야 합니다.
1. 프로젝트 초기화
// 프로젝트 디렉토리 생성 및 초기화
mkdir blog-project
cd blog-project
npm init -y
// 필수 패키지 설치
npm install express mongoose ejs express-session passport passport-local bcryptjs dotenv
npm install --save-dev nodemon
2. 프로젝트 구조 설정
blog-project/
├── models/
│ ├── User.js
│ └── Post.js
├── routes/
│ ├── auth.js
│ ├── posts.js
│ └── index.js
├── views/
│ ├── layout.ejs
│ ├── index.ejs
│ ├── login.ejs
│ ├── register.ejs
│ └── post.ejs
├── public/
│ ├── css/
│ └── js/
├── config/
│ └── database.js
├── .env
├── app.js
└── package.json
3. 환경 변수 설정
// .env 파일
PORT=3000
MONGODB_URI=mongodb://localhost:27017/blog
SESSION_SECRET=your_secret_key_here
단계별 구현 과정
Step 1: Express 서버 및 MongoDB 연결 설정
MongoDB와 Express.js로 블로그 만들기의 첫 번째 단계는 서버를 구성하는 것입니다.
// app.js
const express = require('express');
const mongoose = require('mongoose');
const session = require('express-session');
const passport = require('passport');
require('dotenv').config();
const app = express();
// MongoDB 연결
mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB 연결 성공'))
.catch(err => console.error('MongoDB 연결 실패:', err));
// 미들웨어 설정
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('public'));
app.set('view engine', 'ejs');
// 세션 설정
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24시간
}));
// Passport 초기화
app.use(passport.initialize());
app.use(passport.session());
// 라우트 설정
app.use('/', require('./routes/index'));
app.use('/auth', require('./routes/auth'));
app.use('/posts', require('./routes/posts'));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`서버가 포트 ${PORT}에서 실행 중입니다`);
});
Step 2: 데이터 모델 정의
Mongoose를 사용하여 User와 Post 스키마를 정의합니다.
// models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', UserSchema);
// models/Post.js
const mongoose = require('mongoose');
const PostSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
tags: [{
type: String,
trim: true
}],
views: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
});
// 업데이트 시간 자동 갱신
PostSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});
module.exports = mongoose.model('Post', PostSchema);
Step 3: 사용자 인증 구현
Passport.js를 사용하여 로그인, 회원가입 기능을 구현합니다.
// config/passport.js
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');
const User = require('../models/User');
module.exports = function(passport) {
passport.use(
new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) {
return done(null, false, { message: '등록되지 않은 이메일입니다' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return done(null, false, { message: '비밀번호가 일치하지 않습니다' });
}
return done(null, user);
} catch (err) {
return done(err);
}
})
);
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
};
// routes/auth.js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const passport = require('passport');
const User = require('../models/User');
// 회원가입 페이지
router.get('/register', (req, res) => {
res.render('register');
});
// 회원가입 처리
router.post('/register', async (req, res) => {
const { username, email, password, password2 } = req.body;
try {
// 유효성 검사
if (password !== password2) {
return res.render('register', { error: '비밀번호가 일치하지 않습니다' });
}
// 중복 확인
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.render('register', { error: '이미 등록된 이메일입니다' });
}
// 비밀번호 암호화
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// 사용자 생성
const newUser = new User({
username,
email,
password: hashedPassword
});
await newUser.save();
res.redirect('/auth/login');
} catch (err) {
console.error(err);
res.render('register', { error: '회원가입 중 오류가 발생했습니다' });
}
});
// 로그인 페이지
router.get('/login', (req, res) => {
res.render('login');
});
// 로그인 처리
router.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/auth/login',
failureFlash: false
}));
// 로그아웃
router.get('/logout', (req, res) => {
req.logout((err) => {
if (err) return next(err);
res.redirect('/');
});
});
module.exports = router;
Step 4: 블로그 포스트 CRUD 구현
게시글 생성, 읽기, 수정, 삭제 기능을 구현합니다.
// routes/posts.js
const express = require('express');
const router = express.Router();
const Post = require('../models/Post');
// 인증 확인 미들웨어
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/auth/login');
}
// 전체 게시글 목록
router.get('/', async (req, res) => {
try {
const posts = await Post.find()
.populate('author', 'username')
.sort({ createdAt: -1 })
.limit(20);
res.render('posts/index', { posts });
} catch (err) {
console.error(err);
res.status(500).send('서버 오류');
}
});
// 게시글 작성 페이지
router.get('/new', ensureAuthenticated, (req, res) => {
res.render('posts/new');
});
// 게시글 생성
router.post('/', ensureAuthenticated, async (req, res) => {
const { title, content, tags } = req.body;
try {
const newPost = new Post({
title,
content,
author: req.user._id,
tags: tags ? tags.split(',').map(tag => tag.trim()) : []
});
await newPost.save();
res.redirect('/posts');
} catch (err) {
console.error(err);
res.status(500).send('게시글 작성 실패');
}
});
// 게시글 상세 보기
router.get('/:id', async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('author', 'username email');
if (!post) {
return res.status(404).send('게시글을 찾을 수 없습니다');
}
// 조회수 증가
post.views += 1;
await post.save();
res.render('posts/show', { post });
} catch (err) {
console.error(err);
res.status(500).send('서버 오류');
}
});
// 게시글 수정 페이지
router.get('/:id/edit', ensureAuthenticated, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).send('게시글을 찾을 수 없습니다');
}
// 작성자 확인
if (post.author.toString() !== req.user._id.toString()) {
return res.status(403).send('권한이 없습니다');
}
res.render('posts/edit', { post });
} catch (err) {
console.error(err);
res.status(500).send('서버 오류');
}
});
// 게시글 수정
router.put('/:id', ensureAuthenticated, async (req, res) => {
const { title, content, tags } = req.body;
try {
const post = await Post.findById(req.params.id);
if (post.author.toString() !== req.user._id.toString()) {
return res.status(403).send('권한이 없습니다');
}
post.title = title;
post.content = content;
post.tags = tags ? tags.split(',').map(tag => tag.trim()) : [];
await post.save();
res.redirect(`/posts/${post._id}`);
} catch (err) {
console.error(err);
res.status(500).send('수정 실패');
}
});
// 게시글 삭제
router.delete('/:id', ensureAuthenticated, async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (post.author.toString() !== req.user._id.toString()) {
return res.status(403).send('권한이 없습니다');
}
await Post.findByIdAndDelete(req.params.id);
res.redirect('/posts');
} catch (err) {
console.error(err);
res.status(500).send('삭제 실패');
}
});
module.exports = router;
Step 5: 프론트엔드 뷰 구성
EJS 템플릿으로 사용자 인터페이스를 구성합니다.
<!-- views/layout.ejs -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= typeof title !== 'undefined' ? title : '나의 블로그' %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<nav>
<div class="container">
<a href="/">홈</a>
<a href="/posts">게시글</a>
<% if (user) { %>
<a href="/posts/new">글쓰기</a>
<a href="/auth/logout">로그아웃</a>
<% } else { %>
<a href="/auth/login">로그인</a>
<a href="/auth/register">회원가입</a>
<% } %>
</div>
</nav>
<main class="container">
<%- body %>
</main>
<footer>
<p>© 2025 나의 블로그. All rights reserved.</p>
</footer>
</body>
</html>
테스트 및 배포
로컬 테스트
개발 서버를 실행하여 기능을 테스트합니다.
// package.json에 스크립트 추가
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
}
// 개발 서버 실행
npm run dev
배포 준비
MongoDB와 Express.js로 블로그 만들기 프로젝트를 배포하기 위한 설정입니다.
// 프로덕션 환경 설정
// Heroku, AWS, DigitalOcean 등에 배포 가능
// 1. MongoDB Atlas 설정 (클라우드 MongoDB)
// 2. 환경 변수 설정
// 3. process.env.NODE_ENV === 'production' 체크
// app.js에 추가
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1);
// HTTPS 강제
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
}
보안 강화
// 추가 보안 패키지 설치
npm install helmet express-rate-limit
// app.js에 추가
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
app.use(helmet());
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100 // 최대 100개 요청
});
app.use(limiter);
마무리 및 확장 아이디어
MongoDB와 Express.js로 블로그 만들기 프로젝트를 완성했습니다! 이제 기본적인 블로그 기능을 갖춘 웹 애플리케이션을 만들 수 있습니다. 다음 단계로 댓글 기능, 좋아요 기능, 파일 업로드, 검색 기능, 페이지네이션, 카테고리별 분류 등을 추가해보세요. 또한 React나 Vue.js를 프론트엔드로 분리하여 SPA(Single Page Application)로 전환할 수도 있습니다. 이 프로젝트는 실무 포트폴리오로 활용하기에 최적이며, 개인 블로그나 기술 블로그를 직접 운영하는 기반이 됩니다. 지속적으로 기능을 추가하고 리팩토링하면서 실력을 향상시켜보세요!
📚 함께 읽으면 좋은 글
MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!
📅 2025. 10. 30.
🎯 MongoDB와 Express.js로 블로그 만들기
MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!
📅 2025. 10. 22.
🎯 MongoDB와 Express.js로 블로그 만들기
MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!
📅 2025. 10. 14.
🎯 MongoDB와 Express.js로 블로그 만들기
실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!
📅 2025. 10. 29.
🎯 실시간 채팅 앱 만들기 with Socket.io
30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!
📅 2025. 10. 28.
🎯 30분만에 만드는 Todo App 완성 가이드
💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!
📢 이 글이 도움되셨나요? 공유해주세요!
여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨
🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏
💬 여러분의 소중한 의견을 들려주세요!
MongoDB와 Express.js로 블로그 만들기 관련해서 궁금한 점이 더 있으시다면 언제든 물어보세요!
⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨
🔔 블로그 구독하고 최신 글을 받아보세요!
🌟 프로젝트 아이디어부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨
📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!