본문 바로가기

Node.js

Node.js(15) - Backend MVC 패턴 구현, class(상속 및 추상화)를 활용한 댓글 Front 화면 구현

728x90
반응형

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"));