MongoDB를 사용한 간단한 Node.js 게시판 만들기

Jun 19, 2024
MongoDB를 사용한 간단한 Node.js 게시판 만들기

MongoDB

몽고DB는 NoSQL 데이터베이스로 직접 설치하여 사용할 수 있지만 클라우드 서비스로 제공하는 Atlas를 이용할 수도 있다. NoSQL은 Not Only SQL을 의미하며 SQL이 아닌 다양한 쿼리 방법을 제공한다는 의미를 강조한 표현이다.

MongoDB Atlas

MongoDB Atlas는 기본적으로 1개의 무료 클러스터를 제공한다. 공부하면서 부득이하게 계정을 여러 개 만들 일이 생기는데 구글 이메일의 일종의 트릭을 사용할 수 있다. 나의 이메일 주소에 +를 붙여 임의의 문자를 추가하여도 정상적으로 이메일이 동작한다.
example+dev@gmail.com example+prod@gmail.com

Cluster

클러스터는 여러 개의 MongoDB 서버 인스턴스를 그룹화하여 데이터베이스를 분산, 복제 및 확장할 수 있는 구조이다. 클러스터는여러 데이터베이스를 포함할 수 있다.
주요 유형은 다음과 같다.
  1. Replica Set : 동일한 데이터를 복제하여 여러 개의 서버를 제공하여 고가용성을 보장한다.
  1. Sharded Cluster : 데이터를 샤드 단위로 나누어 저장하여 대규모 데이터를 효율적으로 관리한다.

데이터베이스 구조

  1. Database : 클러스터 내에 존재하며, 데이터베이스는 여러 컬렉션을 포함할 수 있다.
  1. Collection : 데이터베이스 내에 존재하며, 컬렉션은 여러 도큐먼트를 포함할 수 있다.
  1. Document : 컬렉션 내에 존재하는 데이터 저장의 기본 단위이다. 각 도큐먼트는 JSON과 유사한 형식으로 데이터를 저장한다.

Node.js 연결하기

MongoDB에서는 Node.js에서 데이터베이스에 접근할 수 있도록 패키지를 제공한다.
npm i mongodb
notion image
notion image
MongDB 접속 주소는 웹 클라이언트에서 [Connect] 버튼을 클릭하여 확인할 수 있고 기본적으로 아래와 같이 연결할 수 있다. 해당 함수에
require("dotenv").config(); const mongoClient = require('mongodb').MongoClient; const uri = `mongodb+srv://techit:${process.env.DB_PASSWORD}@techit-cluster.y8xovpv.mongodb.net/?retryWrites=true&w=majority&appName=techit-cluster`; // 데이터베이스 연결 mongoClient.connect(uri) .then(client => { console.log('Connected to MongoDB'); });
데몬 프로세스
데몬 프로세스는 백그라운드에서 자동으로 실행되며, 시스템 부팅 시 시작될 수 있고, 사용자가 명시적으로 시작하거나 멈출 필요가 없다.
Node.js에서는 PM2를 이용하여 데몬 프로세스로 관리할 수 있으며 프로덕션에서 많이 사용한다.

Express.js 연결 전략

데이터베이스에 연결되기 전이나 연결에 실패한 상황에서는 서버가 실행되지 않도록 할 수 있다. 하지만 이는 서버 가용성을 저해할 수 있다. 데이터베이스에 접속하지 못하였더라도 서버는 항상 실행 중인 상태여야 한다. 데이터베이스에 연결 실패하더라도 예외 처리를 통해 클라이언트에 적절한 응답을 전송할 수 있다.
하지만 간단한 프로젝트이므로 데이터베이스가 연결되었을 때만 서버가 실행되도록 작성하였다.
require("dotenv").config(); const express = require('express'); const app = express(); const mongoClient = require('mongodb').MongoClient; const uri = `mongodb+srv://techit:${process.env.DB_PASSWORD}@techit-cluster.y8xovpv.mongodb.net/?retryWrites=true&w=majority&appName=techit-cluster`; let db; mongoClient.connect(uri) .then(client => { console.log('Connected to MongoDB'); // 데이터베이스 선택 db = client.db('myboard'); // 테스트 요청 db.collection('post').find().toArray() .then(data => { console.log(data); }); app.listen(8080, function () { console.log("server running at http://127.0.0.1:8080/"); }); }).catch(err => { console.log(err); });

게시글 저장하기

HTML 폼 만들기

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Enter</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> </head> <body class="d-flex justify-content-center align-items-center vh-100"> <div class="col-md-6"> <form action="/save" method="POST"> <div class="mb-3"> <label for="title" class="form-label">제목</label> <input type="text" class="form-control" id="title" name="title" placeholder="제목을 입력하세요"> </div> <div class="mb-3"> <label for="content" class="form-label">내용</label> <textarea class="form-control" id="content" name="content" rows="3" placeholder="내용을 입력하세요"></textarea> </div> <button type="submit" class="btn btn-primary">저장</button> </form> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> </body> </html>
HTTP GET vs. POST
GET 방법으로 폼 요청을 수행하면 쿼리 파라미터가 URL에 노출되어 보안상 좋지 않다. 또한 URL 길이 제한 256자로 존재한다는 말이 있는데 이는 사실이 아니며 HTTP 프로토콜 자체에는 제한을 두지 않는다. 브라우저에 따라 다르지만 대부분 약 2000자로 제한한다.
따라서 POST 방법으로 요청하면 HTTP 요청의 Body에 데이터를 포함한다면 많은 데이터를 안전하게 서버에 전송할 수 있다.

HTTP Body 파싱하기

예전에는 Express.js에서 HTTP Body를 파싱해서 사용하기 위해서 body-parser 패키지를 별도로 설치해야 했지만 최신 버전에서는 의존 모듈로 추가되므로 따로 설치할 필요가 없다.
간단히 미들웨어를 추가하면 req.body 형태로 파싱된 Body를 사용할 수 있다.
const express = require('express'); const app = express(); // 미들웨어 추가 app.use(express.extended({ extended: false })); // 생략 app.post('/save', (req, res) => { console.log(req.body); });

데이터베이스에 저장하기

데이터베이스에 컬렉션을 지정하여 요청으로 전달된 JSON 데이터를 유효성 검사 없이 그대로 저장하였고 저장에 성공하였을 경우 도큐먼트 아이디가 포함된 결과를 반환한다.
하나의 도큐먼트를 삽입할 경우 insertOne을 사용하고 여러 개의 도큐먼트는 insertMany를 통해 배열 형태로 추가하면 된다. 예전에는 그냥 insert를 사용하여 두 가지 역할을 모두 수행하였지만 더 이상 사용되지 않고 명시적 함수로 대체되었다.
app.post('/save', (req, res) => { // 컬렉션에 단일 도큐먼트 추가 db.collection('post').insertOne(req.body).then(result => { console.log('저장완료', result); }); });
{ acknowledged: true, insertedId: new ObjectId('667260a247a31d8c162ca666') }

게시글 목록 표시하기

게시글 목록 뷰

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Home</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> </head> <body> <nav class="navbar navbar-expand-lg bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="#">Navbar</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> </ul> </div> </div> </nav> <table class="table table-hover table-striped text-center container mt-4" style="border: 1px solid;"> <thead> <tr> <th>제목</th> <th>내용</th> <th>삭제</th> </tr> </thead> <tbody> <!-- 넌적스 문법 --> {% for post in posts %} <tr> <td>{{ post.title }}</td> <td>{{ post.content }}</td> <td><button class='delete btn btn-outline-danger' id='{{ post._id }}'>삭제</button></td> </tr> {% endfor %} </tbody> </table> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script> </body> </html>
list.html

게시글 뷰 라우팅

app.get('/list', (req, res) => { db.collection('post').find().toArray() .then(data => { console.log(data); res.json(data); }); });

템플릿 엔진 사용하기

템플릿 엔진은 동적인 결과를 정적인 파일에 담는 것으로 퍼그, 넌적스, ejs 등이 존재한다. 우선 퍼그는 HTML의 전통적인 구조를 너무 망치는 것 같아서 유지보수에 적합하지 않다고 생각한다. 예전에 넌적스를 사용해보았을 때 ejs보다 사용하기 편리했어서 넌적스를 사용하기로 결정하였다.
사실 템플릿 엔진 문법과 기능은 모두 비슷하니 어느 것을 사용하든 크게 상관없다. 하지만 다른 사람이 .ejs 확장자를 보았을 때 생기는 거부감도 있을 것이므로 .html 확장자를 유지할 수 있는 넌적스가 적합하다고 생각한다.
npm i nunjucks
require("dotenv").config(); const express = require('express'); const nunjucks = require('nunjucks'); const app = express(); app.use(express.urlencoded({ extended: false })); // 넌적스 설정 app.set('view engine', 'nunjucks'); nunjucks.configure('views', { express: app, watch: true, }); const mongoClient = require('mongodb').MongoClient; const uri = `mongodb+srv://techit:${process.env.DB_PASSWORD}@techit-cluster.y8xovpv.mongodb.net/?retryWrites=true&w=majority&appName=techit-cluster`; let db; mongoClient.connect(uri) .then(client => { console.log('Connected to MongoDB'); db = client.db('myboard'); db.collection('post').find().toArray() .then(data => { console.log(data); }); app.listen(8080, function () { console.log("server running at http://127.0.0.1:8080/"); }); }).catch(err => { console.log(err); }); app.get('/list', (req, res) => { db.collection('post').find().toArray() .then(data => { console.log(data); // 넌적스 렌더링 (데이터 전달) res.render('list.html', { posts: data }); }); }); app.get('/enter', (req, res) => { console.log(req.body); // 넌적스 렌더링 res.render('enter.html'); }); app.post('/save', (req, res) => { // 단일 도큐먼트 추가 db.collection('post').insertOne(req.body).then(result => { console.log('저장완료', result); // 리다이렉트 res.redirect('/list'); }); });
Share article

blush