MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!

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)로 전환할 수도 있습니다. 이 프로젝트는 실무 포트폴리오로 활용하기에 최적이며, 개인 블로그나 기술 블로그를 직접 운영하는 기반이 됩니다. 지속적으로 기능을 추가하고 리팩토링하면서 실력을 향상시켜보세요!

📚 함께 읽으면 좋은 글

1

MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 30.
🎯 MongoDB와 Express.js로 블로그 만들기

2

MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 22.
🎯 MongoDB와 Express.js로 블로그 만들기

3

MongoDB와 Express.js로 블로그 만들기 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 14.
🎯 MongoDB와 Express.js로 블로그 만들기

4

실시간 채팅 앱 만들기 with Socket.io – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 29.
🎯 실시간 채팅 앱 만들기 with Socket.io

5

30분만에 만드는 Todo App 완성 가이드 – 완성까지 한번에!

📂 프로젝트 아이디어
📅 2025. 10. 28.
🎯 30분만에 만드는 Todo App 완성 가이드

💡 위 글들을 통해 더 깊이 있는 정보를 얻어보세요!

📢 이 글이 도움되셨나요? 공유해주세요!

여러분의 공유 한 번이 더 많은 사람들에게 도움이 됩니다 ✨

🔥 공유할 때마다 블로그 성장에 큰 힘이 됩니다! 감사합니다 🙏

💬 여러분의 소중한 의견을 들려주세요!

MongoDB와 Express.js로 블로그 만들기 관련해서 궁금한 점이 더 있으시다면 언제든 물어보세요!

💡
유용한 정보 공유

궁금한 점 질문

🤝
경험담 나누기

👍
의견 표현하기

⭐ 모든 댓글은 24시간 내에 답변드리며, 여러분의 의견이 다른 독자들에게 큰 도움이 됩니다!
🎯 건설적인 의견과 경험 공유를 환영합니다 ✨

🔔 블로그 구독하고 최신 글을 받아보세요!

📚
다양한 주제
17개 카테고리

정기 업데이트
하루 3회 발행

🎯
실용적 정보
바로 적용 가능

💡
최신 트렌드
2025년 기준

🌟 프로젝트 아이디어부터 다양한 실생활 정보까지!
매일 새로운 유용한 콘텐츠를 만나보세요 ✨

📧 RSS 구독 | 🔖 북마크 추가 | 📱 모바일 앱 알림 설정
지금 구독하고 놓치는 정보 없이 업데이트 받아보세요!

답글 남기기