본문 바로가기

Node.js

Node.js(12) - 비동기 통신 댓글 CRUD, Postman 및 Database 활용

728x90
반응형

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