웹 백엔드

Node.js - MySQL을 연동하여 MVC 구현

토리쟁이 2024. 2. 17. 23:44

 

 

앞서, MVC 패턴의 기본 개념 및 실습에 대한 포스팅에서는 DB를 연결하지 않았기 때문에 Model에 임시 데이터를 넣어서 실습을 진행했었다. 이번 포스팅에서는, MySQL 을 사용하여 실제 DB를 연결해볼 것이다.

 


 

 

이번 실습은 방명록을 등록/수정/삭제하는 페이지를 구현하는 것이다.

 

 

만약 DB를 연결하지 않는다면?

// DB 연결 전
exports.getVisitors = () => {
return [
    { id: 1, name: "allie", comment: "안녕하세요" },
    { id: 2, name: "홍길동", comment: "hi" },
];
};

 

임의로 데이터를 넣은 후, 해당 데이터 배열을 반환하는 함수를 작성하여 임시 DB 역할하도록 작성해야 될 것이다. 이제, 임시 DB를 만들지 않고 실제 DB를 직접 연결해서 사용해보자.

 

 

 

사용할 DB에서 visitor 테이블을 생성하였다.

CREATE TABLE visitor (
    id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(10) NOT NULL,
    comment MEDIUMTEXT
);

 

 

 

 

실습에서는 MySQL 데이터베이스를 사용했기 때문에, mysql 패키지를 설치해야 한다.

$ npm install mysql

 

DB - 서버 연결 객체 생성

mysql.createConnection()을 이용하면, DB와 서버를 연결하는 객체를 생성할 수 있으며 이 객체를 이용하여 각종 쿼리를 실행시킬 수 있다. mysql.createConnection()의 속성으로는 host, port, user, password, database가 들어간다.

  • host: 사용할 DB가 설치된 호스트의 IP
    • mysql 서버가 어디에 있는가? (같은 컴퓨터에 있으면 'localhost' )
  • port: DB를 설치할 때 사용자가 지정한 포트번호(기본값 3306)
  • user: DB의 user이름
  • password: DB를 설치할 때 사용자가 지정한 비밀번호
  • database: 사용할 데이터베이스의 이름

 

여기서 주의할 점은, user:'root' 값으로 설정할 경우, 다음과 같은 오류가 발생한다.

그 이유는, 외부에서 최상위 root 계정으로의 비밀번호 접근을 허용하지 않기 때문이다. 따라서, mysql을 연결하여 사용하기 위해서는 새로운 사용자 계정을 만들고, 그 사용자로 접근을 해야 한다. 새로운 사용자 생성을 위한 코드는 다음과 같다.

-- 새로운 유저 생성
CREATE USER '유저명'@'%' IDENTIFIED BY 'DB비밀번호';

-- 새롭게 만든 유저 정보 변경
-- BY 뒤에 원하는 비밀번호 작성하면 된다.
ALTER USER 'sesac'@'%' IDENTIFIED WITH mysql_native_password BY '변경하고자 하는 비밀번호';

 

유저 생성을 하고 그 유저의 비밀 번호를 변경했지만, 유저 생성과 동시에 비밀번호를 설정하는 방법도 있다.

CREATE USER '유저명'@'%' IDENTIFIED WITH mysql_native_password BY '설정하고자 하는 '비밀번호';

 

 

이렇게 유저 생성을 완료한 후에는, 해당 유저가 모든 DB에 접근 가능하도록 DB 접근 권한을 부여하면 된다.

-- 모든 DB에 접근 가능하도록 sesac 계정에 DB 접근 권한을 부여
GRANT ALL PRIVILEGES ON *.* TO 'sesac'@'%' WITH GRANT OPTION;

-- 현재 사용 중인 sql cash를 삭제하고 새로운 설정 적용
FLUSH PRIVILEGES;

 

  • FLUSH PRIVILEGES; : MySQL 사용자/데이터베이스 등을 생성, 수정, 삭제, 하거나 권한 등을 변경 할 경우, 서버를 재기동 하지 않고 바로 적용하는 명령어

 

 

 

이제 model 코드를 살펴보자.

 

DB 연결 Visitor.js (model > Visitor.js)

// DB 연결
const mysql = require("mysql");

// mysql 연결 설정
// mysql.createConnection({연결정보}) : DB와 서버를 연결하는 객체 생성 -> 이 객체를 이용하여 각종 쿼리 실행 가능
const conn = mysql.createConnection({
  host: "localhost",
  user: "user_name",
  password: "password",
  database: "db_name",
});


// 전체 데이터 조회
// /GET visitors 로 들어왔을 때
// callback함수가 기본으로 들어감
// conn.query('sql문', (error, rows)) : mysql에 쿼리를 실행시켜주는 함수
exports.getVisitors = (cb) => {
  conn.query("SELECT * FROM visitor", (err, rows) => {
    if (err) throw err; // error가 난다면 보여주는 코드

    // rows: 실행 결과
    console.log("Visitor.js 전체목록:", rows);
    // 쿼리의 결과가 나오기 전까지 시간이 걸릴 수 있으므로 callback함수에 넣어서 보내야 함
    cb(rows);
  });
};


// 특정 데이터 1개 조회
exports.getVisitor = (id, cb) => {
  conn.query(`SELECT * FROM visitor WHERE id=${id}`, (err, rows) => {
    if (err) throw err; // error가 난다면 보여주는 코드

    // rows: 실행 결과
    console.log("Visitor.js 데이터 1개 조회", rows); // [{}] : 하나의 데이터를 찾아도 배열 형태로 넘어옴
    // 쿼리의 결과가 나오기 전까지 시간이 걸릴 수 있으므로 callback함수에 넣어서 보내야 함

    cb(rows[0]); // 배열 안에 들어있는 하나의 데이터 객체를 전송
  });
};


// 데이터 등록
// POST /visitor
exports.postVisitor = (data, cb) => {
  // data = {name: ~~, comment: ~~}
  conn.query(
    // 인자로 쿼리, 함수가 들어감
    `INSERT INTO visitor VALUES(NULL, '${data.name}', '${data.comment}')`,
    (err, rows) => {
      if (err) throw err;
      console.log("Visitor.js post", rows);

	// rows에는 message, protocol41 등 여러 속성이 존재하지만, 식별을 위해 그 중 insertId 값만 전송
      cb(rows.insertId);
    }
  );
};


// 데이터 삭제
// delete /visitor
exports.deleteVisitor = (id, cb) => {
  console.log("받아오는 id", id);
  conn.query(`DELETE FROM visitor WHERE id=${id}`, (err, rows) => {
    if (err) throw err;
    console.log("visitor.js delete", rows);

    cb(rows);
  });
};


// 데이터 수정
// patch /visitor
exports.patchVisitor = (data, cb) => {
  console.log("model patchVisitor data 확인", data);
  conn.query(
    `UPDATE visitor SET name='${data.name}', comment = '${data.comment}' WHERE id=${data.id}`,
    (err, rows) => {
      if (err) throw err;
      console.log("Cvisitor.js patch", rows);

      cb(rows);
    }
  );
};

 

 

  • 객체.query('sql문', callback함수);
    • callback함수의 인자로는 (err, rows, fields)가 들어간다.
    • err는 에러
    • rows는 쿼리 실행 결과의 행을 배열 형태로 받는다.
    • fields는 쿼리 실행 결과의 열을 배열 형태로 받는다.(실습에서는 사용하지 않았음)
  • model에서 쿼리를 실행한 후, callback함수에 쿼리 실행 결과를 담아  controller에 전송하는 로직

 

 

 

 

컨트롤러 코드는 다음과 같다.

controller > Cvisitor.js

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

exports.main = (req, res) => {
  res.render("index");
};

// /GET visitors
exports.getVisitors = (req, res) => {
  // visitor.ejs에 데이터 보내주기
  // DB 연결 전 임시 데이터베이스
  // console.log( Visitor.getVisitors() ); // [(), {}] 배열 안에 여러 개의 객체가 들어있는 형태
  //  res.render("visitor", { visitorInfo: Visitor.getVisitors() });

  // DB 연결 후
  // result: model에서 작성한 쿼리 결과값인 rows
  Visitor.getVisitors((result) => {
    // console.log(result);
    res.render("visitor", { visitorInfo: result });
  });
};

// 특정 방명록 1개 조회
// /GET visitor/:id
exports.getVisitor = (req, res) => {
  Visitor.getVisitor(req.params.id, (result) => {
    console.log("Cvisitor.js 1개의 데이터 조회", result);
    res.send(result);
  });
};

// 요청 방법이 다르면, 경로가 같아도 상관없음
// POST /visitor
exports.postVisitor = (req, res) => {
  console.log(req.body); // {id, name, comment}
  Visitor.postVisitor(req.body, (result) => {
    // result는 모델에서 쿼리 실행 후 cb함수 인자로 날린 rows.insertId
    console.log("Cvisitor.js post", result);
    console.log("req.body 확인", req.body);
    res.send({ id: result, name: req.body.name, comment: req.body.comment });
  });
};

// DELETE /visitor
exports.deleteVisitor = (req, res) => {
  console.log("삭제함수 req.body", req.body);
  console.log(req.body.id);
  Visitor.deleteVisitor(req.body.id, (result) => {
    console.log("Cvisitor.js delete", result);
    res.send(result + "번 방명록 삭제 완료");
  });
};

// PATCH /visitor
exports.patchVisitor = (req, res) => {
  console.log("수정함수 req.body", req.body); // {id, name, comment}
  Visitor.patchVisitor(req.body, (result) => {
    console.log("Cvisitor.js patch", result);

    res.send("수정 완료");
  });
};

 

  • 데이터 조회: get 요청, Select문 사용
  • 데이터 등록: post 요청, Insert문 사용
  • 데이터 삭제: delete 요청, Delete문 사용
  • 데이터 수정: patch 요청, Update문 사용
  • 청 방식만 다르다면, 똑같은 경로를 사용해도 문제가 없다.
  • routes에서 지정해 놓은 경로로 지정한 방식의 요청이 들어왔을 때 controller의 함수가 동작한다.
  • 해당 controller의 함수들은 요청받은 내용을 바탕으로 작성해놓은 model의 함수를 호출하여 쿼리를 실행시키고, 그 실행 결과를 result로 받아 그 result를 view에게 전송하는 것이다.
  • 즉, 클라이언트로부터 요청을 받으면 controllermodel과 통신하여 데이터를 처리하고, 응답받은 데이터를 view에게 보냄으로써 model과 view의 중간다리 역할을 하게 되는 것이다.
  • controller의 모든 함수들은 res.send()로 어떠한 결과를 view에게로 응답했지만, 굳이 어떠한 응답을 보내지 않아도 되는 경우, res.end()로 응답 종료를 하면 된다.(응답을 하지 않겠다고 해서 응답 종료없이 끝내는 것은 안됨)

 

 

 

routes > index.js

const express = require("express");
const router = express.Router();

const controller = require("../controller/Cvisitor");

// 해당 경로로 들어왔을 때, 컨트롤러 호출
// 렌더링, get 요청
router.get("/", controller.main);
router.get("/visitors", controller.getVisitors);

// 등록
router.post("/visitor", controller.postVisitor);
// 삭제
router.delete("/visitor", controller.deleteVisitor);
// 수정
router.patch("/visitor", controller.patchVisitor);
// 수정을 위한 1개의 데이터 조회
router.get("/visitor/:id", controller.getVisitor);

module.exports = router;

 

 

 

view는 html에 작성한 js script를 불러와서 사용했다. 

views > visitor.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>방명록 페이지</title>
    <!-- axios CDN -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<!-- custom js-->
<script defer src="/static/visitor.js"></script>  
</head>
  <body>
    <form name="visitor-form">
      <h1>방명록 페이지</h1>
      <fieldset>
        <legend>방명록 등록</legend>
        <input type="text" name="name" placeholder="사용자 이름" />
        <br />
        <input type="text" name="comment" placeholder="간단 방명록 작성" />
        <br />
        <div id="btn-group">
          <button type="button" onclick="createVisitor()">방명록 등록</button>
        </div>
      </fieldset>
    </form>
    <br />
    <br />
    <table border="1" cellspacing="0">
      <thead>
        <!--제목-->
        <tr>
          <th>ID</th>
          <th>작성자</th>
          <th>방명록</th>
          <th>수정</th>
          <th>삭제</th>
        </tr>
      </thead>
      <tbody>
        <!--for문 작성-->
        <%for(let i=0; i<visitorInfo.length; i++){%>
            <tr id="tr_<%= visitorInfo[i].id%>">
            <td><%= visitorInfo[i].id%></td>
            <td><%= visitorInfo[i].name%></td>
            <td><%= visitorInfo[i].comment%></td>
            <td>
                <button type="button" onclick="editVisitor('<%=visitorInfo[i].id%>')">수정</button>
            </td>
            <td>
                <button type="button" onclick="deleteVisitor(this,'<%=visitorInfo[i].id%>')">삭제</button>
            </td>
        </tr>
            <%}%>
      </tbody>
    </table>
  </body>
</html>

 

  • view에서는 controller부터 응답받은 data인 visitorInfo를 배열의 길이만큼 화면에 출력한다.
  • 수정 및 삭제는 버튼에 onclick함수를 걸어 axios 방식으로 controller와 통신하였다.
    • static > visitor.js 코드 참고

 

 

 

static > visitor.js

const tbody = document.querySelector("tbody");

// 방명록 등록
// POST /visitor
function createVisitor() {
  const form = document.forms["visitor-form"];
  
  // 유효성 검사
  if (form.name.value.length === 0 || form.comment.value.length === 0) {
    alert("이름과 방명록을 모두 기입해주세요");
    return;
  }
  if (form.name.value.length > 6) {
    alert("이름은 6글자 이하로 입력해주세요");
    return;
  }

  axios({
    method: "POST",
    url: "/visitor",
    data: {
      name: form.name.value,
      comment: form.comment.value,
    },
  })
    .then((response) => {
      console.log("data확인", response.data); // 실제 데이터는 response 객체의 data에 들어있음 {id, name, comment}
      // 구조 분해 할당
      const { data } = response;
      const html = `
    <tr>
            <td>${data.id}</td>
            <td>${data.name}</td>
            <td>${data.comment}</td>
            <td>
                <button type="button" onclick="editVisitor(${data.id})">수정</button>
            </td>
            <td>
                <button type="button" onclick="deleteVisitor(this,${data.id})">삭제</button>
            </td>
        </tr>
    `;

      tbody.insertAdjacentHTML("beforeend", html); // js에서 기본으로 제공함
    })
    .catch((err) => console.log(err));
}

// 방명록 삭제
// DELETE /visitor
function deleteVisitor(btn, id) {
  axios({
    method: "delete",
    url: "/visitor",
    data: { id: id },
  }).then((res) => {
    console.log(res.data);

    // 실제 요소 지우기
    // remove
    // 1. parentElement 사용
    btn.parentElement.parentElement.remove();

    // closest()
    // 특정 선택자를 가진 가장 가까운 조상 요소를 찾음
    btn.closest(`tr_${id}`).remove();
  });
}

// 방명록 수정
// PATCH /visitor
// 1. 수정을 위한 입력창으로 변경
// 2. 수정 버튼을 누르면, 기존 데이터가 input의 value로 들어가게끔
// => 1개의 데이터 조회 필요

const btnGroup = document.querySelector("#btn-group");

// GET /visiotor/:id
function editVisitor(id) {
  console.log("id확인", id);

  axios({
    method: "get",
    url: `/visitor/${id}`,
  }).then((res) => {
    const { data } = res;
    console.log("res확인", res);
    console.log("data확인", data);

    const form = document.querySelector('form[name="visitor-form"]');
    console.log("form확인", form);
    form.name.value = data.name;
    form.comment.value = data.comment;
  });

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

// PATCH /visitor
function editDo(id) {
  const form = document.querySelector('form[name="visitor-form"]');

  axios({
    url: "/visitor",
    method: "patch",
    data: {
      id: id,
      comment: form.comment.value,
      name: form.name.value,
    },
  }).then((res) => {
    // 수정 결과를 프론트에 반영
    const children = document.querySelector(`#tr_${id}`).children;
    children[1].textContent = form.name.value;
    children[2].textContent = form.comment.value;

    editCancel();
  });
}

function editCancel() {
  const form = document.querySelector('form[name="visitor-form"]');

  // input 초기화
  form.name.value = "";
  form.comment.value = "";

  // 등록 버튼으로 변경
  btnGroup.innerHTML = `<button type="button" onclick="createVisitor()">방명록 등록</button>`;
}

 

  • 방명록 등록 메서드인 createVisitor()에서는 입력받은 작성자명과 방명록 내용을 data 객체에 담아 axios 방식으로 post요청을 controller에게 보낸다. 그 후, 응답받은 data를 화면에 출력하기 위해서 html 객체를 생성하여 선택해놓았던 tbody 객체에 삽입한다. html을 삽입하기 위해 insertAdjacent(); 함수를 사용하였다.
  • insertAdjacentHTML(position, text);
    • 지정된 text를 HTML or XML로 파싱하여 결과 노드들을 지정된 position의 DOM 트리에 삽입
    • position: 요소와 상대적인 위치를 나타내는 문자열
      • "beforebegin": 요소 이전에 위치
      • "afterbegin": 요소 바로 안에서 처음 자식 이전에 위치
      • "beforeend": 요소 바로 안에서 마지막 자식 이후에 위치
      • "afterend": 요소 이후에 위치
    • text: HTML or XML로 파싱되고 트리에 삽입되는 문자열
  • 방명록 삭제 메서드인 deleteVisitor()에서는 인자로 전달받은 해당 방명록의 id를 data 객체에 담아 axios 방식으로 delete 요청을 controller에게 보낸다. DB에서 해당 데이터를 삭제 후, 화면에서도 삭제시키기 위해 클릭 이벤트가 일어난 요소에 함수를 걸어 실제 요소를 삭제한다. (1번 방법을 적용)
    • 1. 해당요소의 부모의 부모요소를 찾아 삭제 → parentElement, remove() 사용
    • 2. 특정 선택자를 가진 가장 가까운 조상 요소를 찾아 삭제 →   closest(), remove() 사용
  • 방명록 수정을 위한 메서드인 editVisitor()에서는 인자로 전달받은 id를 url에 담아 get 요청을 보낸다. 이 함수는 수정할 특정 데이터를 조회하기 위함이다. controller에서는 req.params.id로 파라미터로 넘어온 id를 model에 있는 특정 데이터 1개를 조회하는 getVisitor()함수의 인자로 보낸 후, 결과값으로 조회된 데이터를 응답받는다. 해당 데이터는 다시 view의 js파일로 넘어오고 해당 데이터를 작성자와 방명록 내용을 작성하는 폼에 입력시킨다.
    • 수업 내용은 이렇지만, 개인적으로는 애초에 수정 버튼을 클릭할 때, 해당 방명록의 작성자와 내용을 함수의 인자로 동시에 넘긴다면 굳이 DB에서 해당 데이터를 조회하지 않아도 잘 동작할 것 같다. 다만, 방명록의 내용이 길어질 경우엔 좋지 않을 수 있을 것 같기도 하다..
  • 방명록 수정 메소드인 editDo()는 인자로 id 값을 받고 해당 id값과 작성자, 방명록 내용을 data 객체로 묶어 controller에 patch 요청을 보낸다, 그 후, 수정 결과를 화면에 띄우기 위해 요소를 선택 후, textContent를 사용해 값을 바꿔준다. 

 

 

 

 

최종 페이지

 

 


참고

 

https://gongbu-ing.tistory.com/32

 

https://velog.io/@shitaikoto/SQL-Nodejs-MySQL

 

https://velog.io/@nayonsoso/Node.js-%EB%A1%9C-MySQL-%EC%A0%9C%EC%96%B4%ED%95%98%EA%B8%B0

 

insertAdjacentHTML()

https://developer.mozilla.org/ko/docs/Web/API/Element/insertAdjacentHTML

'웹 백엔드' 카테고리의 다른 글

Node.js - Sequelize  (0) 2024.02.20
Node.js - 환경변수(dotenv와 .env 사용)  (0) 2024.02.19
Node.js - MVC 기본 개념 및 구현  (0) 2024.02.14
Node.js - 게시판 글 등록 및 삭제  (0) 2024.02.03
Node.js - 파일 업로드  (0) 2024.01.31