1) Node.js
1-1) Back-end MVC 패턴 구현
1-2) 댓글 front 화면 구현 - class 활용(상속 및 추상화)
1) Node.js
1-1) Back-end MVC 패턴 구현
back
server.js
const express = require("express");
const app = express();
const cors = require("cors");
const router = require("./routes/comment.route");
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(
cors({
origin: true,
credentials: true,
})
);
app.use(router);
app.use((err, req, res, next) => {
res.status(500).json({
message: err.message,
});
});
app.listen(3000, () => {
console.log("server start");
});
routes/comment.route.js
const express = require("express");
const router = express.Router();
const controller = require("../comment/comment.controller");
router.get("/comments", controller.list);
router.post("/comments", controller.write);
router.get("/comments/:id", controller.view);
router.put("/comments/:id", controller.modify);
router.delete("/comments/:id", controller.delete);
module.exports = router;
comment/comment.controller.js
const service = require("./comment.service");
exports.list = async (req, res, next) => {
try {
const list = await service.getList();
res.json(list);
} catch (e) {
next(e);
}
};
exports.write = async (req, res, next) => {
try {
const userid = `hsb7722`;
const { content } = req.body;
const comment = await service.writeComment(userid, content);
res.json(comment);
} catch (e) {
next(e);
}
};
exports.view = async (req, res, next) => {
try {
const { id } = req.params;
const [comment] = await service.getView(id);
res.json(comment);
} catch (e) {
next(e);
}
};
exports.modify = async (req, res, next) => {
try {
const { id } = req.params; // DB 식별자
const { content } = req.body;
const data = await service.modifyData(content, id);
res.json(data);
} catch (e) {
next(e);
}
};
exports.delete = async (req, res, next) => {
try {
const { id } = req.params;
const data = await service.deleteData(id);
res.json(data);
} catch (e) {
next(e);
}
};
comment/comment.service.js
const repository = require("./comment.repository");
exports.getList = async () => {
try {
const [list] = await repository.findAll();
return list;
} catch (e) {
console.log(e.message);
}
};
exports.writeComment = async (userid, content) => {
try {
if (!content) throw new Error("content 없음!");
const result = await repository.createData(userid, content);
return result;
} catch (e) {
console.log(e.message);
}
};
exports.getView = async (id) => {
try {
const result = await repository.findOne(id);
return result;
} catch (e) {
console.log(e.message);
}
};
exports.modifyData = async (content, id) => {
try {
const [{ changedRows }] = await repository.changeData(content, id);
if (changedRows <= 0)
throw new Error("수정된 데이터가 없습니다. id를 다시 확인해주세요!!");
return { result: changedRows };
} catch (e) {
console.log(e.message);
}
};
exports.deleteData = async (id) => {
try {
const [{ affectedRows }] = await repository.removeData(id);
if (affectedRows <= 0)
throw new Error("삭제된 데이터가 없습니다. id를 다시 확인해주세요!!");
return { result: affectedRows };
} catch (e) {
console.log(e.message);
}
};
comment/comment.repository.js
const pool = require("../models");
exports.findAll = async () => {
try {
const listSql = await pool.query(
`SELECT id, userid, content, DATE_FORMAT(register,'%Y-%m-%d') as register FROM Comment`
);
return listSql;
} catch (e) {
console.log(e.message);
}
};
exports.createData = async (userid, content) => {
try {
const writeSql = `INSERT INTO Comment(userid, content) VALUES('${userid}','${content}')`;
const [{ insertId }] = await pool.query(writeSql);
const [[response]] = await pool.query(
`SELECT id, userid, content, DATE_FORMAT(register,'%Y-%m-%d') as register FROM Comment WHERE id=${insertId}`
);
return response;
} catch (e) {
console.log(e.message);
}
};
exports.findOne = async (id) => {
try {
const viewSql = await pool.query(
`SELECT id, userid, content, DATE_FORMAT(register,'%Y-%m-%d') as register FROM Comment WHERE id=${id}`
);
return viewSql;
} catch (e) {
console.log(e.message);
}
};
exports.changeData = async (content, id) => {
try {
const modifySql = await pool.query(
`UPDATE Comment SET content='${content}' WHERE id='${id}'`
);
return modifySql;
} catch (e) {
console.log(e.message);
}
};
exports.removeData = async (id) => {
try {
const deleteSql = await pool.query(`DELETE FROM Comment WHERE id=${id}`);
return deleteSql;
} catch (e) {
console.log(e.message);
}
};
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;
1-2) 댓글 front 화면 구현 - class 활용(상속 및 추상화)
Front-end
Javascript 문법이 잘되어 있어야지만 가능하다!
물론 front-end 역시 요청/응답의 흐름이 중요하다!
Back-end
요청/응답의 흐름이 중요하다!
추상화
함수를 어떻게 사용할지 정의 혹은 선언만 해놓은 것이 "추상화"이다!!
지하철 노선도를 떠올리면 추상화를 이해하기 좋다!
const setState = () => {};
const render = () => {};
상속
추상화와 상속은 함께 많이 쓰인다!
상속은 아래 코드를 예로 들면 여기서 class 새의 constructor(생성자 함수) 내용이
class 참새 혹은 class 비둘기의 prototype 속성에 값으로 그대로 담기는 것을 의미한다!!
class 새 {
constructor() {
this.wing = 2
this.fly = true
this.leg = 2
}
}
class 참새 extends 새 {
constructor() {
super() // 해당 class의 인스턴스를 호출시키는 역할(해당 코드의 경우, class 새의 인스턴스를 호출하여 새의 인스턴스를 생성시킴)
}
}
class 비둘기 extends 새 {
constructor() {
super()
}
}
class 새 {
constructor(flight) {
this.wing = 2
this.fly = flight // 결과값: this.fly = false
this.leg = 2
}
}
class 참새 extends 새 {
constructor() {
super(false) // super에 다음과 같이 인수값으로 "false"를 넣으면 이것이 class 새의 인스턴스를 호출하고, 그 결과 fly 속성의 값은 "false"가 됨!!
}
}
※ 상속을 할 때는 인스턴스화한 것을 넣어야 한다!!
front
server.js
const express = require("express");
const nunjucks = require("nunjucks");
const app = express();
app.set("view engine", "html");
nunjucks.configure("views", {
express: app,
});
app.use(express.static("public"));
app.use(express.json());
app.get("/", (req, res, next) => {
res.render("index.html");
});
app.use((error, req, res, next) => {
res.send("error");
});
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>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<link type="text/css" rel="stylesheet" href="/css/comment.css" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/js/index.js"></script>
</body>
</html>
public/css/comment.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/index.js
import render from "/js/core/render.js";
import App from "/js/core/class_render.js";
new App(document.querySelector("#app"));
public/js/core/render.js
const app = document.querySelector("#app");
let state = {
list: [
{ id: 1, userid: "wer7722", content: "hello1", register: "2023-01-09" },
{ id: 2, userid: "wer7722", content: "hello2", register: "2023-01-09" },
{ id: 3, userid: "wer7722", content: "hello3", register: "2023-01-09" },
],
user: {
userid: "hsb7722",
username: "sangbeom",
},
};
const setState = (newState) => {
// state 변수가 안 바뀌었으면 render 함수를 호출하지 않겠다!
if (state === newState) return;
state = { ...state, ...newState };
render();
};
const render = () => {
const { list } = state;
// const comments = list.map((comment) => {
// return `
// <ul class="comment-row" data-index="${comment.id}">
// <li class="comment-id">${comment.userid}</li>
// <li class="comment-content">${comment.content}</li>
// <li class="comment-date">${comment.register}</li>
// </ul>`;
// });
// app.innerHTML = comments.join("");
app.innerHTML = `
<form>input</form>
<div id="comment-list">
${list
.map((comment) => {
return `
<ul class="comment-row" data-index="${comment.id}">
<li class="comment-id">${comment.userid}</li>
<li class="comment-content">${comment.content}</li>
<li class="comment-date">${comment.register}</li>
</ul>`;
})
.join("")}
</div>
<button id="btn">버튼!</button>
`;
document.querySelector("#btn").addEventListener("click", () => {
setState({
list: [
...list,
{ id: 4, userid: "wer7722", content: "hello4", register: "2023-01-09" },
],
});
});
};
export default render;
public/js/core/class_render.js
class Component {
target; // 앞으로 넣은 element
state; // 앞으로 쓸 데이터들
constructor(_target) {
this.target = _target;
this.setup();
this.render();
}
setup() {} // 자식 클래스에서 구현할 것임!!
template() {} // 자식 클래스에서 구현할 것임!!
render() {
this.target.innerHTML = this.template();
}
setState(newState) {
if (this.state === newState) return;
this.state = { ...this.state, ...newState };
this.render();
}
// 속성
// target
// state
// method
// render
// setState
// template
}
// App class를 만든 뒤 Component class를 상속받음!
class App extends Component {
/*
// 자식 클래스(여기서 App)에 인자값을 넣었는데 이것이 부모 클래스(Component)의 매개변수 개수와 같으먼 생성자 함수(constructor)는 생략 가능하다!!
constructor(target) {
super(target);
}
*/
setup() {
this.state = {
list: [
{ id: 1, userid: "wer7722", content: "hello1", register: "2023-01-09" },
{ id: 2, userid: "wer7722", content: "hello2", register: "2023-01-09" },
{ id: 3, userid: "wer7722", content: "hello3", register: "2023-01-09" },
],
user: {
userid: "web7722",
username: "sangbeom",
},
};
}
template() {
const { list } = this.state;
return `
<form>input</form>
<div id="comment-list">
${list
.map((comment) => {
return `
<ul class="comment-row" data-index="${comment.id}">
<li class="comment-id">${comment.userid}</li>
<li class="comment-content">${comment.content}</li>
<li class="comment-date">${comment.register}</li>
</ul>`;
})
.join("")}
</div>
<button id="btn">버튼!</button>
`;
}
}
// App class 인스턴스 생성
// const a = new App(document.querySelector("#app"));
// console.dir(a);
export default App;
class Component와 class App 파일 분리
public/js/core/Component.js
class Component {
target; // 앞으로 넣은 element
state; // 앞으로 쓸 데이터들
constructor(_target) {
this.target = _target;
this.setup();
this.render();
this.setEvent();
}
setup() {} // 자식 클래스에서 구현할 것임!!
template() {} // 자식 클래스에서 구현할 것임!!
mounted() {}
render() {
this.target.innerHTML = this.template();
this.mounted();
}
setEvent() {}
addEvent(type, selector, callback) {
console.log(selector);
console.log(document.querySelectorAll(selector));
const children = [...document.querySelectorAll(selector)];
const isTarget = (target) =>
children.includes(target) || target.closest(selector);
this.target.addEventListener(type, (e) => {
if (!isTarget(e.target)) return false;
callback(e);
});
// 논리 연산자
// const port = process.env.PORT || 3000 // 결과값: true 값에 해당하는 것이 변수 "port"에 담김!!
// const a = null || 10 || undefined // 결과값: 10
// const b = null && 10 && undefined // 결과값: null
// const c = console.log("hello") || 10 // 결과값: 10이 들어가지만 그 전에 "hello"가 콘솔에 찍힘
// const d = 10 || console.log("hello") // 결과값: 10이 들어감(10이 이미 true 값으로 확인되었기에 뒤의 "console.log('hello')"를 평가하지 않기 때문!!)
}
setState(newState) {
if (this.state === newState) return;
this.state = { ...this.state, ...newState };
this.render();
}
// 속성
// target
// state
// method
// render
// setState
// template
}
export default Component;
public/js/app.js
import Component from "/js/core/Component.js";
class App extends Component {
/*
// 생성자 함수(constructor)는 생략 가능하다!!
constructor(target) {
super(target);
}
*/
setup() {
this.state = {
list: [
{
id: 1,
userid: "wer7722",
content: "hello1",
register: "2023-01-09",
updated: false,
},
{
id: 2,
userid: "wer7722",
content: "hello2",
register: "2023-01-09",
updated: false,
},
{
id: 3,
userid: "wer7722",
content: "hello3",
register: "2023-01-09",
updated: false,
},
],
user: {
userid: "web7722",
username: "sangbeom",
},
};
}
content(content) {
return `
<span class="comment-update-btn">${content}</span>
<span class="comment-delete-btn">❌</span>`;
}
update(content) {
return `
<span>
<input type="text" class="comment-update-input" value=${content} data-value="" />
</span>
<span class="comment-delete-btn">❌</span>`;
}
template() {
const { list } = this.state;
return `
<ul class='comment'>
<li class='comment-form'>
<form id='commentFrm'>
<h4>
댓글쓰기
<span></span>
</h4>
<span class='ps_box'>
<input type='text' class='int' name='content' placeholder='댓글 입력하세요~'>
</span>
<button type='submit' class='btn'>등록</button>
</form>
</li>
<li id='comment-list'>
${list
.map((comment) => {
return `<ul class="comment-row" data-index="${comment.id}">
<li class="comment-id">${comment.userid}</li>
<li class="comment-content">
${
comment.updated
? this.update(comment.content)
: this.content(comment.content)
}
</li>
<li class="comment-date">${comment.register}</li>
</ul>`;
})
.join("")}
</li>
</ul>
`;
}
mounted() {
console.log("render 완료되면 무조건 실행됨!!");
}
setEvent() {
/*
// 첫번째 코드
this.target.querySelector(".btn").addEventListener("click", () => {
console.log("click~~~");
const { list } = this.state;
this.setState({
list: [
...list,
{
id: 4,
userid: "wer7722",
content: "hello4",
register: "2023-01-09",
},
],
});
});
*/
/*
// 두번째 코드
this.target.addEventListener("click", (e) => {
const list = [...this.state.list];
if (e.target.classList.contains("btn")) {
list.push({
id: 4,
userid: "wer7722",
content: "hello4",
register: "2023-01-09",
});
this.setState({ list });
}
});
*/
// 세번째 코드
// this.addEvent("click", ".btn", (e) => {
// console.log("click~~!!");
// });
this.addEvent("submit", "#commentFrm", (e) => {
e.preventDefault();
// console.log(e.target.content.value);
const { list } = this.state;
this.setState({
list: [
...list,
{
id: list.length + 1,
userid: "wer7722",
content: e.target.content.value,
register: "2023-01-09",
updated: false,
},
],
});
});
this.addEvent("click", ".comment-update-btn", (e) => {
const ul = e.target.closest(".comment-row");
const { index } = ul.dataset;
const list = [...this.state.list];
const newList = list.map((v) => {
if (v.id === parseInt(index)) v.updated = true;
return v;
});
console.log(newList);
this.setState({ list: newList });
});
this.addEvent("click", ".comment-delete-btn", (e) => {
const ul = e.target.closest(".comment-row");
const { index } = ul.dataset;
// 요청 주고
// 응답 결과
const list = [...this.state.list].filter((v) => v.id !== parseInt(index));
this.setState({ list });
});
this.addEvent("keypress", ".comment-update-input", (e) => {
if (e.keyCode !== 13) return;
const ul = e.target.closest(".comment-row");
const index = parseInt(ul.dataset.index);
// 요청
// 응답
const list = this.state.list.map((v) => {
if (v.id === index) {
v.content = e.target.value;
v.updated = false;
}
return v;
});
this.setState({ list });
});
// document.querySelector("click", () => {});
}
}
export default App;
public/js/index.js
import App from "/js/app.js";
new App(document.querySelector("#app"));