1) Node.js
1-1) 비동기 통신 - 댓글 CRUD(Postman 활용)
1-2) Database 활용
1) Node.js
1-1) 비동기 통신 - 댓글 CRUD(Postman 활용)
비동기 통신(callback or Promise or async/await)
- 초기 세팅 : front(3005번 port)와 back(3000번 port) 디렉토리를 만들고 front, back 디렉토리 각각 터미널을 키고 시작
XMLHttpRequest --> fetch --> axios
→ 위의 내용들이 잘 숙달되면 back-end를 잘 만드는 스킬들을 배울 예정!
기존 게시판 CRUD
GET /board/list
POST /board/write
GET /board/view
POST /board/modify?idx=1
POST /board/delete
RESTful API(일종의 규칙)를 적용한 router(path 부분이 간결해진다는 장점이 있음!!)
GET /comments
POST /comments
GET /comments/:id
PUT /comments/:id
DELETE /comments/:id
+) 참고
controllers
services
repository
--> 위의 세 디렉토리(controllers, services, repository)를 "comment" 디렉토리에 넣어주면 코드 관리 및 수정이 용이함!!
front
server.js
const express = require("express");
const nunjucks = require("nunjucks");
const app = express();
app.use(express.static("public"));
app.set("view engine", "html");
nunjucks.configure("views", {
express: app,
});
app.get("/", (req, res, next) => {
res.render("index.html");
});
app.listen(3005, () => {
console.log("server start");
});
views/index.html
<!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>Document</title>
<link href="/css/index.css" rel="stylesheet" />
</head>
<body>
<div>
<ul class="comment">
<li class="comment-form">
<form id="commentFrm">
<h4>
댓글쓰기
<span></span>
</h4>
<span class="ps_box">
<input
type="text"
placeholder="댓글 내용을 입력해주세요."
class="int"
name="content"
/>
</span>
<input type="submit" class="btn" value="등록" />
</form>
</li>
<li id="comment-list"></li>
</ul>
</div>
<template id="commentRow">
<ul class="comment-row" data-index="1">
<li class="comment-id"></li>
<li class="comment-content"></li>
<li class="comment-date"></li>
</ul>
</template>
<template id="content-baisc">
<span class="comment-update-btn">입력입력입력~~~</span>
<span class="comment-delete-btn">❌</span>
</template>
<template id="content-update">
<span>
<input type="text" class="comment-update-input" data-value="" />
</span>
<span class="comment-delete-btn">❌</span>
</template>
<script src="/js/comment.js" type="text/javascript"></script>
</body>
</html>
public/css/index.css
* {
margin: 0;
padding: 0;
}
ul,
li {
list-style: none;
}
.comment {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
padding: 30px;
width: 600px;
margin: 0 auto;
}
.comment > li {
margin-top: 20px;
}
.comment > li:nth-child(1) {
margin: 0px;
}
.comment-row {
display: flex;
justify-content: space-between;
flex-direction: row;
}
.comment-row {
margin-top: 20px;
width: 100%;
}
.comment-row > li:nth-child(2) {
flex-shrink: 0;
flex-grow: 1;
padding-left: 25px;
z-index: 1;
width: 100%;
}
.comment-row > li:nth-child(2) {
width: 85px;
}
.comment-form > form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.comment-form > form > h4 {
width: 100%;
margin: 14px 0 14px 0;
}
.comment-content {
cursor: pointer;
word-break: break-all;
padding-right: 25px;
}
.ps_box {
display: block;
position: relative;
width: 80%;
height: 51px;
border: solid 1px #dadada;
padding: 10px 14px 10px 14px;
background: #fff;
box-sizing: border-box;
}
.ps_box > input {
outline: none;
}
.int {
display: block;
position: relative;
width: 100%;
height: 29px;
padding-right: 25px;
line-height: 29px;
border: none;
background: #fff;
font-size: 15px;
box-sizing: border-box;
z-index: 10;
}
.btn {
width: 18%;
padding: 18px 0 16px;
text-align: center;
box-sizing: border-box;
text-decoration: none;
border: none;
background: #333;
color: #fff;
font-size: 14px;
}
.comment-delete-btn {
display: inline-block;
margin-left: 7px;
cursor: pointer;
}
.comment-update-input {
border: none;
border-bottom: 1px solid #333;
font-size: 16px;
color: #666;
outline: none;
}
public/js/comment.js
const commentFrm = document.querySelector("#commentFrm");
const commentList = document.querySelector("#comment-list");
const state = [];
class Comment {
constructor(content) {
this.userid = "web7722";
this.Content = content;
this.updated = false;
this.now = new Date();
}
getToday(separator = "") {
const date = this.now;
let mm = date.getMonth() + 1;
let dd = date.getDate();
return [
date.getFullYear(),
(mm > 9 ? "" : "0") + mm,
(dd > 9 ? "" : "0") + dd,
].join(separator);
}
set Content(value) {
if (value.length === 0) throw new Error("content를 채워주세요.");
this.content = value;
}
}
const setTotalRecord = () => {
const span = document.querySelector("h4 > span");
span.innerHTML = `(${state.length})`;
};
const getBox = (flag, content) =>
!flag ? createContentBox(content) : createUpdateBox(content);
// 일반적인 페이지
function createContentBox(content) {
// const selector = "#" + "content-baisc"
const template = document.querySelector("#content-baisc");
const clone = document.importNode(template.content, true);
const span = clone.querySelectorAll("span");
span[0].innerHTML = content;
return clone;
}
// 수정
function createUpdateBox(content) {
const template = document.querySelector("#content-update");
const clone = document.importNode(template.content, true);
const input = clone.querySelector("span > input");
input.addEventListener("keyup", enterHandler);
input.value = content;
return clone;
}
function enterHandler(e) {
if (e.keyCode !== 13) return;
try {
const { index } = e.target.parentNode.parentNode.parentNode.dataset;
const { value } = e.target;
state[index].Content = value;
state[index].updated = !state[index].updated;
// CRUD 중 U(UPDATE) 부분에 해당됨!
drawing();
} catch (e) {
alert(e.message);
}
}
function createRow(index) {
const template = document.querySelector("#commentRow");
const clone = document.importNode(template.content, true);
const ul = clone.querySelector("ul");
const li = clone.querySelectorAll("li");
const item = state[index];
ul.dataset.index = index;
li[0].innerHTML = item.userid;
li[1].innerHTML = "";
li[1].append(getBox(item.updated, item.content));
li[2].innerHTML = item.getToday("-");
return ul;
}
function drawing() {
commentList.innerHTML = "";
for (let i = state.length - 1; i >= 0; i--) {
const row = createRow(i);
commentList.append(row);
}
}
function addState(value) {
try {
const instance = new Comment(value); // 이 부분이 ajax 코드로 바뀌면 됨!
state.push(instance); // // CRUD 중 C(CREATE) 부분에 해당됨!
setTotalRecord();
drawing();
} catch (e) {
alert(e.message);
}
}
function submitHandler(e) {
e.preventDefault();
const { content } = e.target;
const { value } = content;
addState(value);
content.focus();
this.reset();
}
function clickHandler(e) {
const contentNode = e.target.parentNode;
const { index } = contentNode.parentNode.dataset;
switch (e.target.className) {
case "comment-delete-btn":
const flag = confirm("삭제 할꺼야?~");
if (!flag) return;
state.splice(index, 1); // CRUD 중 D(DELETE) 부분에 해당됨!
drawing();
break;
case "comment-update-btn":
state[index].updated = !state[index].updated;
const content = e.target.innerHTML;
contentNode.innerHTML = "";
const item = getBox(state[index].updated, content);
contentNode.append(item);
break;
}
}
setTotalRecord();
commentList.addEventListener("click", clickHandler);
commentFrm.addEventListener("submit", submitHandler);
back
server.js
const express = require("express");
const cors = require("cors");
const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(cors());
/*
GET /comments
POST /comments
GET /comments/:id
PUT /comments/:id
DELETE /comments/:id
*/
app.get("/comments", (req, res, next) => {
try {
res.send("전체 댓글 가져오기");
} catch (e) {
next(e);
}
});
app.post("/comments", (req, res, next) => {
try {
const userid = req.body.userid;
// (중요!!) throw를 통해 throw 바로 뒤에 이어서 나오는 내용("new Error('userid가 없습니다!!')")이 아래 catch 구문의 매개변수 "e(혹은 error)"에 담기는 구조이다!!
// 이후 해당 값이 매개변수 "e"에 담겨 next 함수가 실행되어 router 맨 아래에 위치한 에러 처리 구문을 실행(해당 에러 처리 구문의 "error" 매개변수에 값이 담긴 뒤 res.send를 실행시키는 것을 의미함)시켜 에러를 출력하는 구조이다!!
if (!userid) throw new Error("userid가 없습니다!!"); // {message: "userid가 없습니다!!"}
res.send(req.body);
} catch (e) {
// console.log(e);
next(e);
}
});
app.get("/comments/:id", (req, res, next) => {
try {
res.send("댓글 하나 가져오기");
} catch (e) {
next(e);
}
});
app.put("/comments/:id", (req, res, next) => {
try {
res.send("댓글 수정하기");
} catch (e) {
next(e);
}
});
app.delete("/comments/:id", (req, res, next) => {
try {
res.send("댓글 삭제하기");
} catch (e) {
next(e);
}
});
app.use((error, req, res, next) => {
// console.log(error)
console.log(error.message);
res.send(`${error}`);
});
app.listen(3000, () => {
console.log("server start");
});
Postman
HTTP 프로토콜에 맞게끔 요청을 해주는 것이 "Postman"이다!!
1-2) Database 활용
MySQL 접속
$ mysql -uroot -p
Database 생성
$ CREATE DATABASE comments;
특정 Database 사용
$ USE comments;
Table 생성
$ CREATE TABLE Comment(
id INT(11) PRIMARY KEY AUTO_INCREMENT,
userid VARCHAR(30) NOT NULL,
content TEXT NOT NULL,
register DATETIME NOT NULL DEFAULT now()
);
Comment Table Field
- id
- userid
- content
- register
front
server.js
const express = require("express");
const nunjucks = require("nunjucks");
const app = express();
app.use(express.static("public"));
app.set("view engine", "html");
nunjucks.configure("views", {
express: app,
});
app.get("/", (req, res, next) => {
res.render("index.html");
});
app.listen(3005, () => {
console.log("server start");
});
views/index.html
<!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>Document</title>
<link href="/css/index.css" rel="stylesheet" />
</head>
<body>
<div>
<ul class="comment">
<li class="comment-form">
<form id="commentFrm">
<h4>
댓글쓰기
<span></span>
</h4>
<span class="ps_box">
<input
type="text"
placeholder="댓글 내용을 입력해주세요."
class="int"
name="content"
/>
</span>
<input type="submit" class="btn" value="등록" />
</form>
</li>
<li id="comment-list"></li>
</ul>
</div>
<template id="commentRow">
<ul class="comment-row" data-index="1">
<li class="comment-id"></li>
<li class="comment-content"></li>
<li class="comment-date"></li>
</ul>
</template>
<template id="content-baisc">
<span class="comment-update-btn">입력입력입력~~~</span>
<span class="comment-delete-btn">❌</span>
</template>
<template id="content-update">
<span>
<input type="text" class="comment-update-input" data-value="" />
</span>
<span class="comment-delete-btn">❌</span>
</template>
<script src="/js/comment.js" type="text/javascript"></script>
</body>
</html>
public/css/index.css
* {
margin: 0;
padding: 0;
}
ul,
li {
list-style: none;
}
.comment {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
padding: 30px;
width: 600px;
margin: 0 auto;
}
.comment > li {
margin-top: 20px;
}
.comment > li:nth-child(1) {
margin: 0px;
}
.comment-row {
display: flex;
justify-content: space-between;
flex-direction: row;
}
.comment-row {
margin-top: 20px;
width: 100%;
}
.comment-row > li:nth-child(2) {
flex-shrink: 0;
flex-grow: 1;
padding-left: 25px;
z-index: 1;
width: 100%;
}
.comment-row > li:nth-child(2) {
width: 85px;
}
.comment-form > form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.comment-form > form > h4 {
width: 100%;
margin: 14px 0 14px 0;
}
.comment-content {
cursor: pointer;
word-break: break-all;
padding-right: 25px;
}
.ps_box {
display: block;
position: relative;
width: 80%;
height: 51px;
border: solid 1px #dadada;
padding: 10px 14px 10px 14px;
background: #fff;
box-sizing: border-box;
}
.ps_box > input {
outline: none;
}
.int {
display: block;
position: relative;
width: 100%;
height: 29px;
padding-right: 25px;
line-height: 29px;
border: none;
background: #fff;
font-size: 15px;
box-sizing: border-box;
z-index: 10;
}
.btn {
width: 18%;
padding: 18px 0 16px;
text-align: center;
box-sizing: border-box;
text-decoration: none;
border: none;
background: #333;
color: #fff;
font-size: 14px;
}
.comment-delete-btn {
display: inline-block;
margin-left: 7px;
cursor: pointer;
}
.comment-update-input {
border: none;
border-bottom: 1px solid #333;
font-size: 16px;
color: #666;
outline: none;
}
public/js/comment.js
const commentFrm = document.querySelector("#commentFrm");
const commentList = document.querySelector("#comment-list");
const state = [];
// callback, Promise, async/await
// const pr = new Promise((resolve, reject) => {});
const request = ({ method, path, body }) => {
return new Promise((resolve, reject) => {
const host = "http://localhost:3000";
const xhr = new XMLHttpRequest();
xhr.open(method, `${host}${path}`); // http://localhost:3000/comments
xhr.setRequestHeader("Content-type", "application/json");
xhr.send(JSON.stringify(body));
// 백그라운드에서 콜스택으로 넘어올 때 아래 xhr.onload 코드가 실행되는 구조이다!!
// "xhr.onload"는 pending 상태이다!
xhr.onload = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
resolve(JSON.parse(xhr.response));
} else {
reject("에러입니다!!!");
}
};
});
};
/*
// callback을 활용한 코드
const request = ({ method, path, body }, callback) => {
const host = "http://localhost:3000";
const xhr = new XMLHttpRequest();
xhr.open(method, `${host}${path}`); // http://localhost:3000/comments
xhr.setRequestHeader("Content-type", "application/json");
xhr.send(JSON.stringify(body));
// 백그라운드에서 콜스택으로 넘어올 때 아래 xhr.onload 코드가 실행되는 구조이다!!
xhr.onload = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
callback(JSON.parse(xhr.response));
}
};
};
*/
class Comment {
constructor(content) {
this.userid = "web7722";
this.Content = content;
this.updated = false;
this.now = new Date();
}
getToday(separator = "") {
const date = this.now;
let mm = date.getMonth() + 1;
let dd = date.getDate();
return [
date.getFullYear(),
(mm > 9 ? "" : "0") + mm,
(dd > 9 ? "" : "0") + dd,
].join(separator);
}
set Content(value) {
if (value.length === 0) throw new Error("content를 채워주세요.");
this.content = value;
}
}
const setTotalRecord = () => {
const span = document.querySelector("h4 > span");
span.innerHTML = `(${state.length})`;
};
const getBox = (flag, content) =>
!flag ? createContentBox(content) : createUpdateBox(content);
// 일반적인 페이지
function createContentBox(content) {
// const selector = "#" + "content-baisc"
const template = document.querySelector("#content-baisc");
const clone = document.importNode(template.content, true);
const span = clone.querySelectorAll("span");
span[0].innerHTML = content;
return clone;
}
// 수정
function createUpdateBox(content) {
const template = document.querySelector("#content-update");
const clone = document.importNode(template.content, true);
const input = clone.querySelector("span > input");
input.addEventListener("keyup", enterHandler);
input.value = content;
return clone;
}
function enterHandler(e) {
if (e.keyCode !== 13) return;
try {
const { index } = e.target.parentNode.parentNode.parentNode.dataset;
const { value } = e.target;
state[index].Content = value;
state[index].updated = !state[index].updated;
// CRUD 중 U(UPDATE) 부분에 해당됨!
drawing();
} catch (e) {
alert(e.message);
}
}
function createRow(index) {
const template = document.querySelector("#commentRow");
const clone = document.importNode(template.content, true);
const ul = clone.querySelector("ul");
const li = clone.querySelectorAll("li");
const item = state[index];
ul.dataset.index = index;
li[0].innerHTML = item.userid;
li[1].innerHTML = "";
li[1].append(getBox(item.updated, item.content));
li[2].innerHTML = item.register;
return ul;
}
function drawing() {
commentList.innerHTML = "";
for (let i = state.length - 1; i >= 0; i--) {
const row = createRow(i);
commentList.append(row);
}
}
async function addState(value) {
try {
// const instance = new Comment(value); // 이 부분이 ajax 코드로 바뀌면 됨!
// state.push(instance); // // CRUD 중 C(CREATE) 부분에 해당됨!
const content = value;
/*
// callback을 활용한 코드
request(
{ method: "post", path: "/comments", body: { content } },
(instance) => {
state.push(instance);
setTotalRecord();
drawing();
}
);
*/
/*
// Promise를 활용한 코드
request({ method: "post", path: "/comments", body: { content } })
.then((instance) => {
state.push(instance);
setTotalRecord();
drawing();
})
.catch((err) => {
console.log(err);
})
.finally(() => {});
*/
// async/await을 활용한 코드
const instance = await request({
method: "post",
path: "/comments",
body: { content },
});
state.push(instance);
setTotalRecord();
drawing();
} catch (e) {
alert(e.message);
}
}
function submitHandler(e) {
e.preventDefault();
const { content } = e.target;
const { value } = content;
addState(value);
content.focus();
this.reset();
}
function clickHandler(e) {
const contentNode = e.target.parentNode;
const { index } = contentNode.parentNode.dataset;
switch (e.target.className) {
case "comment-delete-btn":
const flag = confirm("삭제 할꺼야?~");
if (!flag) return;
state.splice(index, 1); // CRUD 중 D(DELETE) 부분에 해당됨!
drawing();
break;
case "comment-update-btn":
state[index].updated = !state[index].updated;
const content = e.target.innerHTML;
contentNode.innerHTML = "";
const item = getBox(state[index].updated, content);
contentNode.append(item);
break;
}
}
setTotalRecord();
commentList.addEventListener("click", clickHandler);
commentFrm.addEventListener("submit", submitHandler);
back
server.js
const express = require("express");
const cors = require("cors");
const app = express();
const mysql = require("./models");
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(cors());
/*
id
userid
content
register
*/
/*
GET /comments
POST /comments
GET /comments/:id
PUT /comments/:id
DELETE /comments/:id
*/
app.get("/comments", (req, res, next) => {
try {
res.send("전체 댓글 가져오기");
} catch (e) {
next(e);
}
});
app.post("/comments", async (req, res, next) => {
try {
const userid = "hsb7722";
const { content } = req.body;
// (중요!!) throw를 통해 throw 바로 뒤에 이어서 나오는 내용("new Error('userid가 없습니다!!')")이 아래 catch 구문의 매개변수 "e(혹은 error)"에 담기는 구조이다!!
// 이후 해당 값이 매개변수 "e"에 담겨 next 함수가 실행되어 router 맨 아래에 위치한 에러 처리 구문을 실행(해당 에러 처리 구문의 "error" 매개변수에 값이 담긴 뒤 res.send를 실행시키는 것을 의미함)시켜 에러를 출력하는 구조이다!!
if (!userid) throw new Error("userid가 없습니다!!"); // {message: "userid가 없습니다!!"}
if (!content) throw new Error("content가 없습니다!!");
const sql = `INSERT INTO Comment(userid, content) VALUES('${userid}', '${content}')`;
// insertId는 AUTO_INCREMENT 값으로 해당 table의 row가 새로 생길 때마다(증가할 때마다) 1씩 증가된 값을 반환함!!
const [{ insertId }] = await mysql.query(sql);
// DATE_FORMAT(register, '%Y-%m-%d')
const [[response]] = await mysql.query(
`SELECT id, userid, content, DATE_FORMAT(register, '%Y-%m-%d') as register FROM Comment WHERE id=${insertId}`
);
/*
// 바로 아래 구문과 동일하게 동작하기에 필요에 따라 편한 방식으로 쓰면 됨!
res.json({
...response,
updated: false,
});
*/
response.updated = false;
res.json(response);
} catch (e) {
// console.log(e);
next(e);
}
});
app.get("/comments/:id", (req, res, next) => {
try {
res.send("댓글 하나 가져오기");
} catch (e) {
next(e);
}
});
app.put("/comments/:id", (req, res, next) => {
try {
res.send("댓글 수정하기");
} catch (e) {
next(e);
}
});
app.delete("/comments/:id", (req, res, next) => {
try {
res.send("댓글 삭제하기");
} catch (e) {
next(e);
}
});
// 위의 next의 첫번째 인자값이 아래 error 매개변수에 들어간 뒤 res.send가 실행되는 구조이다!
app.use((error, req, res, next) => {
// console.log(error)
console.log(error.message);
res.send(`${error}`);
});
app.listen(3000, () => {
console.log("server start");
});
models/index.js
const mysql = require("mysql2");
const pool = mysql
.createPool({
host: "127.0.0.1",
port: "3306",
user: "root",
password: "MySQL 비밀번호",
database: "comments",
})
.promise();
module.exports = pool;
※ "동기"처럼 코드를 작성하기 위해 등장한 것이 "async/await"이다!!
Postman
'Node.js' 카테고리의 다른 글
Node.js(14) - back 댓글 CRUD 구현, 모듈 및 class를 활용한 댓글 front 화면 구현 (0) | 2023.01.08 |
---|---|
Node.js(13) - XMLHttpRequest/fetch/axios, 비동기 통신 댓글 CRUD(axios 활용) (0) | 2023.01.05 |
Node.js(11) - AJAX 기본 포맷, server 분리(front/back) 구현 (0) | 2023.01.03 |
Node.js(10) - express를 활용한 게시판 구현(CRUD) 및 Router 나누기 (0) | 2022.12.20 |
Node.js(9) - express를 활용한 게시판 구현 (0) | 2022.12.19 |