Node.js와 MySQL을 활용한 MVC 패턴

  • 앞서 작성한 생성, 조회, 수정, 삭제의 내용을 전체적으로 살펴봅니다.


1. 프로젝트 구조

16-mvc_mysql/
├── controller/
│   └── Cvisitor.js
├── model/
│   └── Visitor.js
├── routes/
│   └── index.js
├── views/
│   ├── 404.ejs
│   ├── index.ejs
│   └── visitor.ejs
├── static/
│   └── visitor.js
├── app.js
├── package.json
└── package-lock.json


2. 구조별 살펴보기

1) controller/CVisitor.js

const Visitor = require('../model/Visitor');

// (1) GET / => localhost:PORT/
exports.getMain = (req, res) => {
  res.render('index');
};

// (2) GET /visitor => localhost:PORT/visitor
exports.getVisitors = (req, res) => {
  // [before]
  // res.render('visitor', { data: Visitor.getVisitors() });

  // [after]
  Visitor.getVisitors((result) => {
    // result 매개변수
    // : model/Visitor.js의 getVisitors 함수의 callback(rows)의 "rows" 변수에 대응.
    console.log(`controller/Cvisitor.js >> `, result);

    console.log(Visitor); // { getVisitors: [Function (anonymous)] }
    console.log(Visitor.getVisitors); // [Function (anonymous)]
    

    // res.send(`test`);

    res.render(`visitor`, { data: result });
    // 함수의 결과 값을 data에 대입하여야 하는데 Visitor.getVisitors를 입력하게 되면
    // 함수 자체를 입력하는 것이 되기 때문에 오류가 발생한다.
  })
};

// GET /visitor/:id
exports.getVisitor = (req, res) => {
  // req.params.id // 조회 해야할 id
  Visitor.getVisitor(req.params.id, (result) => {
    res.send(result);
  }); 

}


// POST
exports.postVisitor = (req, res) => {
  console.log(req.body);

  Visitor.postVisitor(req.body, (result) => {
    // result: rows.insertId
    
    console.log(`controller/Cvisitor.js >> ` , result);
    // controller/Cvisitor.js >> 4

    res.send({
      id : result,
      name : req.body.name,
      comment : req.body.comment
    })
  });
}

// Patch
exports.patchVisitor = (req, res) => {
  console.log(req.body);

  Visitor.patchVisitor(req.body, (result) => {
    console.log(`controller/Cvisitor.js >> `, result);

    res.send({ result }); // { result : result }

  });
}


// Delete
exports.deleteVisitor = (req, res) => {
  console.log(req.body);

  Visitor.deleteVisitor(req.body.id, (result) => {
    console.log('controller/Cvisitor.js >> ', result);
    
    res.send({ result }); // { result : result }
  })


2) model/Visitor.js

// [before]
// // (임시) DB로부터 방명록 데이터를 받아옴
// exports.getVisitors = () => {
//     return [
//       { id: 1, name: '홍길동', comment: '내가 왔다.' },
//       { id: 2, name: '이찬혁', comment: '으라차차' },
//     ];
// };    

// [after]
const mysql = require(`mysql`);
const conn = mysql.createConnection({
  host: `localhost`,
  user: `user`,
  password: `12345678`,
  database: `codingon`
}); // database 연결 객체

exports.getVisitors = (callback) => {
  conn.query(`select * from visitor`, (err, rows) => {
    if(err) throw err;

    console.log(`model/Visitor.js >>  `, rows);
    callback(rows);
  })
}


exports.getVisitor = (targetId, callback)  => {
  conn.query(`select * from visitor where id=${targetId}`, (err, row) => {
    if(err) throw err;

    console.log(`model/Visitor.js >>`, row);
    // model/Visitor.js >> [ RowDataPacket { id: 3, name: '이수현', comment: '아뵤뵤뵤' } ]
    callback(row[0]); // 배열 형태이기 때문에 [0] 사용
  })

}


// POST 로직
exports.postVisitor = (data, callback) => {
  console.log(data)
  conn.query(`insert into visitor(name, comment) values('${data.name}','${data.comment}')`,
    (err, rows) => {
      if (err) throw err;

      console.log('model/Visitor.js >> ', rows);
      // model/Visitor.js >>  OkPacket {
      //   fieldCount: 0,
      //   affectedRows: 1,
      //   insertId: 4, // -> pk
      //   serverStatus: 2,
      //   warningCount: 0,
      //   message: '',
      //   protocol41: true,
      //   changedRows: 0
      // }
      callback(rows.insertId);
    }
  )
}


// PATCH 로직
exports.patchVisitor = (updateData, callback) => {
  const {id, name, comment} = updateData;
  conn.query(
    `update visitor set name='${name}', comment='${comment}' where id=${id}`,
    (err, rows) => {
      if(err) throw err

      console.log(`model/visitor.js >> `, rows);
      callback(true);
    });
}

// DELETE 로직
exports.deleteVisitor = (targetId, callback) => {
  // targetId: 삭제해야할 visitor id
  conn.query(`delete from visitor where id=${targetId}`, (err, rows) => {
    if (err) {
      throw err;
    }

    console.log(`model/Visitor.js >> `, rows);
    // OkPacket {
    //   fieldCount: 0,
    //   affectedRows: 1,
    //   insertId: 0,
    //   serverStatus: 2,
    //   warningCount: 0,
    //   message: '',
    //   protocol41: true,
    //   changedRows: 0
    // }
    callback(true); // 삭제
  })


3) routes/index.js

const express = require('express');
const controller = require('../controller/Cvisitor');
const router = express.Router();

// 작업 순서
// read all -> create -> delete -> read one -> update


// GET / => localhost:PORT/
router.get('/', controller.getMain);

// GET /visitors => localhost:PORT/visitors
router.get('/visitors', controller.getVisitors); // 전체 조회

// GET /visitor/:id
router.get('/visitor/:id', controller.getVisitor); // 방명록 하나 조회

// POST /visitor
router.post(`/visitor`, controller.postVisitor); // 방명록 추가

// PATCH /visitor
router.patch(`/visitor`, controller.patchVisitor); // 방명록 하나 수정


// DELETE /visitor
router.delete(`/visitor`, controller.deleteVisitor); // 방명록 하나 삭제
module.exports = router;


4) static/visitor.js

const tbody = document.querySelector(`tbody`);
const buttonGroup = document.querySelector(`#button-group`);


// 폼의 [등록] 버튼 클릭 시 -> POST /visitor 요청 
function createVisitor() {
    console.log('click 등록 btn')

    const form = document.forms['visitor-form'];

    axios({
        method: 'POST',
        url: '/visitor',
        // 추가하려는 정보를 req.body에 실어서 요청을 보냄
        data: {
            name: form.name.value,
            comment: form.comment.value
        }
    }).then((res) => {
        console.log(res)

        const { data } = res; // {id : ooo, name : ooo, comment : ooo}
        console.log(data)

        const html = `
            <tr id="tr_${data.id}">
                <td>${data.id}</td>
                <td>${data.name}</td>
                <td>${data.comment}</td>
                <td><button type="button" onclick="patchVisitor(${data.id};">수정</button></td>
                <td><button type="button" onclick="deleteVisitor(this, ${data.id});">삭제</button></td>
            </tr>
        `;

        // insertAdjacentHTML : 특정 요소에 html 추가
        tbody.insertAdjacentHTML(`beforeend`, html);
    })
}

// [삭제] 버튼 클릭 시
// - 테이블에서 해당 행 삭제
function deleteVisitor(obj, id) {
    console.log(obj); // 클릭한 삭제 버튼 
    console.log(id); // 행의 id

    if (!confirm('삭제하시겠습니까?')) { // !false: 취소버튼 클릭
        return; // deleteVisitor 함수 종료 -> 백으로 요청하지 않고 그대로 종료
    } 

    axios({
        method: 'DELETE',
        url: '/visitor',
        data: {
            id // id : id
        }
    }).then((res) => {
        console.log(res.data)

        if (res.data.result) {
            alert('삭제 성공!');
            // obj.parentElement.parentElement.remove(); // 버튼(obj)의 부모요소의 부모요소를 삭제하면 행이 삭제되는 개념이다.
            
            obj.closest(`#tr_${id}`).remove(); // 버튼에서 해당되는 요소를 역순으로 찾아주는 매서드
        }
    })
}

// [수정] 버튼 클릭 시
// - form input에 value 넣기
// - [변경], [취소] 버튼 보이기
function editVisitor(id) {
    axios({
        method : 'GET',
        url : `/visitor/${id}`
    }).then(res => {
        console.log(res.data);
        // {
        //     id: 3, 
        //     name: '이수현', 
        //     comment: '아뵤뵤뵤'
        // }

        const {name, comment} = res.data;
        const form = document.forms[`visitor-form`];
        form.name.value = name;
        form.comment.value = comment;
    });

    const html = `
        <button type="button" onclick="editDo(${id});">변경</button>
        <button type="button" onclick="editCancel();">취소</button>
    `;
    buttonGroup.innerHTML = html;
}

// [변경] 버튼 클릭시
// - 데이터 수정 요청 
function editDo(id) {
    const form = document.forms['visitor-form'];

    axios({
        method: 'PATCH',
        url: '/visitor',
        data: {
            id, // id : id
            name : form.name.value,
            comment : form.comment.value
        }
    }).then(res => {
        console.log(res.data);

        if (res.data.result) {
            alert('수정 성공!');

            const children = document.querySelector(`#tr_${id}`).children;
            children[1].textContent = form.name.value; // 이름 열
            children[2].textContent = form.comment.value; // 방명록 열

            // 입력창 초기화
            editCancel();
        }
    })
}


// [취소] 버튼 클릭시
// - input 초기화
// - [등록] 버튼 되돌리기
function editCancel() {
    // 1. input 초기화
    const form = document.forms[`visitor-form`];
    form.name.value = ``;
    form.comment.value = ``;

    // 2. [등록] 버튼 보이기
    const html = `<button type="button" onclick="createVisitor();">등록</button>`;
    buttonGroup.innerHTML = html;    

}


5) views/

(1) views/404.ejs

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>404. 페이지를 찾을 수 없습니다. </h1>

    <p> 죄송합니다. 찾을 수 없는 페이지입니다.</p>

    <a href="/">홈으로 가기</a>
</body>
</html>

(2) views/index.ejs

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>MVC 패턴을 MySQL과 연결해보자</h1>
    <a href="/visitors"> 방문자 목록 보기</a>
</body>
</html>

(3) views/visitor.ejs

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Document</title>
      <!-- axios cdn -->
      <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
    <a href="/">홈으로 가기</a>
    <form name="visitor-form">
        <fieldset>
          <legend>방명록 등록</legend>
          <input type="text" id="name" placeholder="사용자 이름" /> <br>
          <input type="text" id="comment" placeholder="방명록" /> <br>
          <div id="button-group">
            <button type="button" onclick="createVisitor();">등록</button>
          </div>
        </fieldset>
      </form>
      <br>

      <table border="1" cellspacing="0">
        <thead>
          <tr>
            <th>ID</th>
            <th>작성자</th>
            <th>방명록</th>
            <th>수정</th>
            <th>삭제</th>
          </tr>
        </thead>
        <tbody>
          <!-- data: db에서 가지고 오는 데이터 => 새로고침해도 데이터 남아있음 -->
          <% for (let i = 0; i < data.length; i++) { %>
          <tr id="tr_<%= data[i].id %>">
            <td><%= data[i].id %></td>
            <td><%= data[i].name %></td>
            <td><%= data[i].comment %></td>
            <td><button type="button" onclick="editVisitor(`<%= data[i].id %>`);">수정</button></td>
            <td><button type="button" onclick="deleteVisitor(this, `<%= data[i].id %>`);">삭제</button></td>
          </tr>
          <% } %>
        </tbody>
      </table>

      <script src="/static/visitor.js"></script>
</body>
</html>


6) app.js

const express = require(`express`);
const app = express();
const PORT = 8000;

app.set(`view engine`, `ejs`);
app.set(`views`, `./views`);
app.use(`/static`, express.static(__dirname + `/static`));

app.use(express.urlencoded({ extended : true}));
app.use(express.json());

const indexRouter = require(`./routes`);
app.use(`/`, indexRouter);

app.get(`*`, (req, res) => {
    res.render(`404`);
})

app.listen(PORT, () => {
    console.log(`${PORT} 서버 연결 성공`);
})


7) index.sql

show databases;

use codingon;

create table visitor (
	id int primary key auto_increment,
    name varchar(10) not null,
    comment mediumtext
);

desc visitor;

-- 데이터 추가
insert into visitor values
	(null, '홍길동', '내가 왔다.'), 
    (null, '이찬혁', '으라차차');
    
insert into visitor values
	(null, '이수현', '아뵤뵤뵤');

select * from visitor;

-- user 계정 생성
create user 'user'@'%' identified by '12345678'; -- 계정 추가
grant all privileges on *.* to 'user'@'%' with grant option; -- 새로운 계정에 권한 부여
flush privileges; -- 캐쉬 지우고 새로운 설정 적용

alter user 'user'@'%' identified with mysql_native_password by '12345678'; -- 인증 방식 변경

select * from mysql.user;  -- 계정 생성 확인


8) package.json

{
  "name": "16-mvc_mysql",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "ejs": "^3.1.10",
    "express": "^4.19.2",
    "mysql": "^2.18.1"
  }
}


9) package-lock.json

생략