[node.js & express] 게시판 만들기
💪🏻 목표
1) Node를 이용해서 웹 프레임워크를 구성
2) mongoDB와 mongoose를 이용하여 원하는 데이터베이스를 만들고 활용
3) express를 기반으로 CRUD(Create, Read, Update, Delete) 기능이 포함된 REST api 작성
4) AWS에 express와 mongoDB 서비스를 배포
🙌🏻 아쉬운 점
node로 작업한 첫 프로젝트고 아직은 미숙해서 코드가 너무 난잡한 느낌이다.
그리고 에러 코드 출력에 대해서 아직 완벽하게 구현을 못했다. (구현하다가 다른 에러가 발생해서 애쓰다가 우선 마무리했다.)
다음 프로젝트에서는 CRUD를 좀 더 깔끔하게 구현하는 방법. 쓸떼 없는 코드로 낭비하지 않도록 노력해야겠다.
더불어 로그인 관련은 아직 배우지 않았는데 그것에 대한 공부하며 구조에 대한 고민을 해야겠다.
본 글에서는 내가 짰던 코드를 돌아보면서 간단하게나마 설명하는 방식으로 써보겠다.
나중에 실력이 좀 더 쌓이면, 작성하는 순서나 디테일을 기록하겠다.
참고로 본 글 코드는 깃허브에 올려놓았다.
https://github.com/Haksae90/node_project_1
Node.js를 아직 설치하지 못했다면 ➡ Node.js 설치
1. app.js
app.js가 웹 서버이다.
웹 서버에서 해야할 일은
1) 포트 연결
2) db 연결
3) URL 렌더링 파일 연결
4) 라우터 연결
5) 각종 셋팅
- json으로 파싱
- urlencode
- ejs view engine
- 정적 파일 셋팅
const express = require("express");
const connect = require("./schemas");
const app = express();
const port = 3000;
connect(); // db 연결
// articles 라우터 가져오기
const articlesRouter = require("./routes/articles");
// 콘솔 창에 어떤 요청이 있는지에 대해서 보여줌
const requestMiddleware = (req, res, next) => {
console.log("Request URL:", req.originalUrl, " - ", new Date());
next();
};
// 바디로 들어온 제이슨 형태를 파싱
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(requestMiddleware);
// ejs view engine
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
//정적파일 사용
app.use(express.static("./views"));
// article 라우터 사용
app.use("/", [articlesRouter]);
// home 페이지 렌더링
app.get('/', (req, res) => {
res.render('board');
})
// post 페이지 연결
app.get('/post', (req, res) => {
res.render('post');
})
// 콘솔창에 이 포트로 서버가 켜졌다고 알려줌
app.listen(port, () => {
console.log(port, "포트로 서버가 켜졌습니다.");
});
2. routes/articles.js
웹 서버를 통해 라우터로 요청이 들어오고, 모든 API 요청을 여기서 해결한다.
라우터는 깔끔한게 좋아서, 상세 API는 모두 controller에 넣고, 모듈로 불러와서 사용한다.
-> 즉 프론트에서 요청하면 라우터에서 컨트롤러로 요청을 처리할 수 있도록 전달해준다. 그리고 다시 받아서 전달 (이것이 라우터의 역할)
const express = require("express");
const router = express.Router();
const {
getAllArticles,
getDetail,
post,
editPage,
editArticle,
deleteArticle
} = require("../controllers/postController");
// Articles 전체 목록 조회
router.get("/articles", getAllArticles);
// Articles 상세 페이지 조회
router.get("/articles/:_id", getDetail);
// Article 생성
router.post("/post", post);
// Article 수정 페이지 로드
router.get("/edit/:_id", editPage);
// Article 수정
router.put("/articles/edit/:_id", editArticle);
// Article 삭제
router.delete("/articles/delete/:_id", deleteArticle)
module.exports = router;
3. controller/postController.js
API의 실제 기능에 대한 코드는 컨트롤러에 다 들어가 있다.
DB와 연결한 코드 외에는 export 밖에 없다.
기본적으로 try, catch로 에러를 잡았고 특별한 것은 없다.
모든 API를 변수로 선언한 후, 모듈로 라우터와 연결해주었다.
const Articles = require("../schemas/articles");
// Articles 전체 목록 조회
const getAllArticles = async (req, res) => {
try {
const articles = await Articles.find({}).sort("-date").exec();
res.json({
articles,
});
} catch (error) {
res.status(400).send ({ error: error.message})
}
};
// Articles 상세 페이지 조회
const getDetail = async (req, res) => {
try {
const { params: { _id }, } = req;
const article = await Articles.find({ _id: _id });
res.status(200).render('detail', { article });
} catch (error) {
res.status(400).send({ error: error.message });
}
};
// Article 생성
const post = async (req, res) => {
const { title, authorId, password, content } = req.body;
let date = new Date(+new Date() + 3240 * 10000).toISOString().replace("T", " ").replace(/\..*/, '');
await Articles.create({
title,
authorId,
password,
content,
date,
});
res.json({ success: true });
};
// Article 수정 페이지 로드
const editPage = async (req, res) => {
try {
const { params: { _id }, } = req;
const article = await Articles.find({ _id: _id });
res.status(200).render('edit', { article });
} catch (error) {
res.status(400).send({ error: error.message });
}
};
// Article 수정
const editArticle = async (req, res) => {
const { _id } = req.params;
const { authorId, password, title, content } = req.body;
const existsArticle = await Articles.find({ _id });
if (!existsArticle.length) {
return res.status(400).json({ success: false, errorMessage: "해당 게시글은 삭제된 상태입니다."})};
if (Number(password) !== Number(existsArticle[0].password)) {
return res.status(400).json({ success: false, errorMessage: "비밀번호가 틀립니다."})};
await Articles.updateOne({ _id }, { $set: { authorId, title, content } });
res.json({ success: true });
};
// Article 삭제
const deleteArticle = async (req, res) => {
try {
const { _id } = req.params;
const { password } = req.body;
const existsArticle = await Articles.find({ _id });
if (Number(password) === Number(existsArticle[0].password)){
await Articles.deleteOne({ _id });
res.json({ sucess: true });
}} catch (error) {
res.status(400).send({ error: error.message })
}
};
module.exports = {
getAllArticles,
getDetail,
post,
editPage,
editArticle,
deleteArticle
}
4. schema or model
- 몽고 DB는 비관계형 데이터베이스다. schema나 model이 없어도 데이터를 저장할 수 있지만, 균일한 사용을 위해 schema를 사용하면 좋다.
- 우선 몽구스를 불러오고, 연결해준다. 에러 캐치를 해주고 이를 사용할 수 있도록 export
const mongoose = require("mongoose");
const connect = () => {
mongoose.connect("mongodb://localhost:27017/hanghae99", { ignoreUndefined: true}).catch((error) => {
console.error(err);
});
};
module.exports = connect;
- articles의 스키마를 설정해준다.
- required는 필수적이라는 뜻이다.
const mongoose = require("mongoose");
const articlesSchema = mongoose.Schema({
title: {
type: String,
required: true,
},
authorId: {
type: String,
required: true,
},
password: {
type: Number,
required: true,
},
content: {
type: String,
required: true,
},
date:{
type: String,
required: true,
}
});
module.exports = mongoose.model("articles", articlesSchema);
5. ejs
총 4개로 구성되어있다. CSS는 과감히 잘라버리고 올리겠다.
1) board.ejs (홈)
- 웹 서버에 홈 역할을 하는 ejs다. 해당 화면에는 3가지 기능이 있어야한다. 먼저는 모든 ariticle을 보여주는 것, 글쓰기 페이지로 이동, 상세 페이지로 이동
(1) articles 조회
- 모든 아티클을 조회하는 것은 ready 함수와 getArticles 함수를 이용했다. ajax로 GET을 보내고 다시 받아온 정보를 for문을 통해서 작업한 뒤 append 해준다.
(2) 글쓰기 페이지 이동
- 간단하게 window.location.replace를 사용하였다.
(3) 상세 페이지 이동
- (1)에서 apeend 할 때, <a href>를 기입하여 바로 해당 상세페이지로 이동할 수 있도록 작성
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>학새의 항해 블로그</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<!--font-->
<link href="https://fonts.googleapis.com/css2?family=Sunflower:wght@300&display=swap" rel="stylesheet">
</head>
<script>
$(document).ready(function () {
getArticles();
});
function getArticles() {
$.ajax({
type: "GET",
url: "/articles",
data: {},
success: function (response) {
let articles = response["articles"];
for (let i = 0; i < articles.length; i++) {
let authorId = articles[i]["authorId"];
let title = articles[i]["title"];
let date = articles[i]["date"];
let _id = articles[i]["_id"];
let temp_html = `<tr>
<td>"${authorId}"</td>
<td><a href="/articles/${_id}">"${title}"<a/></td>
<td>"${date}"</td>
</tr>`
$("#articleBox").append(temp_html);
}
}
})
}
function post(){
window.location.replace("/post")
}
</script>
<body>
<form class="title_warp">
<h2 class="title">학새의 항해 블로그</h2>
</form> <br>
<div class="button">
<button type="button" class="btn btn-success" onclick="post()">글쓰기</button>
</div>
<section>
<div class="articles">
<table class="table">
<thead>
<tr>
<th scope="col">작성자</th>
<th scope="col">제목</th>
<th scope="col">작성날짜</th>
</tr>
</thead>
<tbody id="articleBox">
</tbody>
</table>
</div>
</section>
</body>
</html>
2) detail.ejs
- board에서 a href를 클릭하면 해당 url이 라우터로 이동하였다가, 컨트롤러에서 상세 페이지 조회 함수로 넘어간다.
- 거기서 url로 온 값을 받아서 DB에서 해당 _id를 검색 후 detail.ejs를 렌더링하고, 해당 article 데이터를 전송한다.
- detail 페이지는 렌더링되면서 article 페이지에 필요한 정보가 작성된다.
- 게시글 수정도 동일한 방식으로 해당 _id를 url로 보내는 방식을 이용한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>학새의 항해 블로그</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<!--font-->
<link href="https://fonts.googleapis.com/css2?family=Sunflower:wght@300&display=swap" rel="stylesheet">
</head>
<script>
// 게시글 수정
function edit() {
window.location.href = "/edit/<%=article[0]._id%>"
}
</script>
<body>
<form class="title_warp">
<h2 class="title">학새의 항해 블로그</h2>
</form> <br>
<section>
<div class="articles">
<table class="table">
<thead>
<tr>
<th scope="col">작성자</th>
<th scope="col">제목</th>
<th scope="col">내용</th>
<th scope="col">작성날짜</th>
</tr>
</thead>
<tbody id="articleBox">
<tr>
<td>
<%=article[0].authorId%>
</td>
<td>
<%=article[0].title%>
</td>
<td>
<%=article[0].content%>
</td>
<td>
<%=article[0].date%>
</td>
</tr>
</tbody>
</table>
</div>
</section> <br>
<!-- Button -->
<div class="button">
<input id="edit" type="button" class="btn btn-success" onclick="edit()" value="수정하기">
</div>
</body>
</html>
3) edit.ejs
- edit 페이지에서 신경써야할 부분은, 해당 DB를 수정하겠다고 하면, post 페이지와 동일한 화면에 미리 저장된 value 값이 띄워져야한다.(비밀번호란 제외) 이를 위해서 ready 함수와 input_Text 함수를 사용했다.
- 해당 페이지는 게시글 수정과 삭제가 가능한데, 둘 다 DB에 저장된 비밀번호와 입력한 비밀번호가 일치해야만 가능하다.
1) 게시글 수정 : 선택자를 통해서 받아온 값을 ajax를 통해서 url에는 _id, body에는 값을 넣어 전송한다.
2) 게시글 삭제 : 비밀번호만 DELETE로 보내고 비밀번호가 일치하면 삭제
<!DOCTYPE html>
<html lang="us">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>글쓰기</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<!--font-->
<link href="https://fonts.googleapis.com/css2?family=Sunflower:wght@300&display=swap" rel="stylesheet">
</head>
<script>
//자동으로 함수 실행
$(document).ready(function () {
input_Text()
})
// 작성자명에 이름 기입
function input_Text() {
document.getElementById("authorId").value = "<%=article[0].authorId%>"
}
// 게시글 수정
function edit(_id) {
let authorId = $("#authorId").val()
let password = $("#password").val()
let title = $("#title").val()
let content = $("#content").val()
$.ajax({
type: "PUT",
url: "/articles/edit/"+_id,
data: {
authorId,
password,
title,
content
},
success: function(response) {
alert("수정이 완료되었습니다");
window.location.replace("/")
}
})
}
function deleteArticle(_id) {
let password = $("#password").val()
$.ajax({
type: "DELETE",
url: "/articles/delete/"+_id,
data: { password },
success: function (response) {
alert("게시글이 삭제되었습니다");
window.location.replace("/")
}
})};
</script>
<body>
<form class="WritePost">
<div class="title_wrap">
<h2 class="title">글쓰기</h2>
</div>
<section class="Basic_info_wrap">
<div class="authorId">
<label class="col-md-12" for="authorId">작성자명</label>
<div class="col-md-12">
<input id="authorId" type="text" placeholder="작성자명을 입력하세요." onclick="input_Text()" value="authorId"
class="form-control input-md" autofocus required> <!--required : 공백입력시 경고-->
</div>
</div>
<br>
<div class="password">
<label class="col-md-12" for="password">비밀번호</label>
<div class="col-md-12">
<input id="password" type="text" placeholder="해당 글의 비밀번호."
class="form-control input-md" autofocus required>
</div>
</div>
</section>
<section class="body_wrap">
<div class="body_form">
<label class="col-md-12" for="title">제목</label>
<div class="col-md-12">
<textarea class="form-control" id="title" placeholder="제목은 간략하고 임팩트있게!"
style="height:100px;" autofocus required><%=article[0].title%></textarea> <br>
</div>
<label class="col-md-12" for="content">내용</label>
<div class="col-md-12">
<textarea class="form-control" id="content" placeholder="쓰고싶은 것을 자유롭게 써주세요."
style="height:100px;" autofocus required><%=article[0].content%></textarea>
</div>
<!-- Button -->
<div class="button">
<input id="resister" class="btn btn-primary" type="submit"
onclick="edit(`<%=article[0]._id%>`)" value="수정완료">
<input id="resister" class="btn btn-primary" type="submit"
onclick="deleteArticle(`<%=article[0]._id%>`)" value="삭제하기">
</div>
</div>
</section>
</form>
</body>
</html>
4) post.ejs
1) 돌아가기, 2) 게시글 작성 기능이 들어가있다.
게시글 작성은 수정과 거의 비슷하게, 선택자를 통해서 정보 받고 Ajax post를 통해서 라우터에 전달한다. 그리고 해당 값은 DB에 저장된다.
<!DOCTYPE html>
<html lang="us">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>글쓰기</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
<!--font-->
<link href="https://fonts.googleapis.com/css2?family=Sunflower:wght@300&display=swap" rel="stylesheet">
</head>
<script>
// 돌아가기
function moveindex(){
window.location.replace("/")
}
// 게시글 작성
function post() {
let authorId = $("#authorId").val()
let password = $("#password").val()
let title = $("#title").val()
let content = $("#content").val()
$.ajax({
type: "POST",
url: "/post",
data: {
authorId,
password,
title,
content
},
success: function (response) {
alert("게시글이 등록되었습니다");
window.location.replace("/")
}
})
}
</script>
<body>
<form class="WritePost">
<div class="title_wrap">
<h2 class="title">글쓰기</h2>
</div>
<section class="Basic_info_wrap">
<div class="authorId">
<label class="col-md-12" for="authorId">작성자명</label>
<div class="col-md-12">
<input id="authorId" type="text" placeholder="작성자명을 입력하세요."
class="form-control input-md" autofocus required> <!--required : 공백입력시 경고-->
</div>
</div>
<br>
<div class="password">
<label class="col-md-12" for="password">비밀번호</label>
<div class="col-md-12">
<input id="password" type="text" placeholder="해당 글의 비밀번호."
class="form-control input-md" autofocus required>
</div>
</div>
</section>
<section class="body_wrap">
<div class="body_form">
<label class="col-md-12" for="title">제목</label>
<div class="col-md-12">
<textarea class="form-control" id="title" placeholder="제목은 간략하고 임팩트있게!"
style="height:100px;" autofocus required></textarea> <br>
</div>
<label class="col-md-12" for="content">내용</label>
<div class="col-md-12">
<textarea class="form-control" id="content" placeholder="쓰고싶은 것을 자유롭게 써주세요."
style="height:100px;" autofocus required></textarea>
</div>
<div class="button">
<input id="resister" class="btn btn-primary" type="submit"
onclick="post()" value="등록하기">
<input id="resister" class="btn btn-primary" type="submit"
onclick="moveindex()" value="돌아가기">
</div>
</div>
</section>
</form>
</body>
</html>