[32] MySQL과 MVC 패턴 연결한 프로젝트(방명록), 문자열 보간법, insertAdjacentHTML()

minjeong·2024년 2월 10일
0
post-thumbnail

MVC 패턴과 MySQL을 연결해서 방명록 프로젝트를 만들어보자.
1. 작성 후 등록을 누르면 DB에 저장된다.(Create)
2. 등록된 내용이 아래 Table에 바로 보인다.(Read)
3. 수정을 누르면 방명록 수정이 가능하다.(Update)
4. 삭제를 누르면 방명록이 삭제된다.(Delete)

1. 세팅

mkdir 폴더명 #폴더 생성
cd 폴더명 #해당폴더로 이동

npm init -y #프로젝트 시작 명령어

npm install express ejs mysql2 # 각각의 패키지 설치
  • 프로젝트의 진입로가 되는 app.js or index.js 파일 생성

2. 폴더 구조 세팅 및 필요한 파일 생성

- 방명록에 필요한 파일 생성

3. 세부내용 작성

app.js

require('dotenv').config();
const express = require('express');
const app = express();
const PORT = 8000;

//미들웨어
app.set('view engine', 'ejs');
app.use(express.json());

//router
const pageRouter = require('./routes/page');
app.use('/', pageRouter);

const visitorRouter = require('./routes/visitor');
app.use('/api/visitor', visitorRouter); //data를 주고받는 router

//오류처리
//오류처리 router는 맨 마지막 router로 선언 -> 나머지 코드 무시되기 때문
app.use('*', (req, res) => { 
    res.status(404).render('404');
});

app.listen(PORT, () => {
    console.log(`http://localhost:${PORT}`);
});

routes/page.js

const express = require('express');
const controller = require('../controller/Cpage');

const router = express.Router();

//GET / => localhost:8000/
router.get('/', controller.main);
//localhost:8000/visitor
router.get('/visitor', controller.pageVisitor);

module.exports = router;

routes/visitor.js

//생성, 조회, 삭제, 수정 포함되어야함

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

const router = express.Router();

//방명록에 관련된 데이터를 처리하는 라우터
//localhost:8000/api/visitor  -> 이 url부터 시작되는 페이지, 이게 root가 됨(app.js에서 정의)
//GET / 방명록 전체 보이기
router.get('/', controller.allVisitor);

//GET /:id 방명록 하나만 보이게 하기
router.get('/:id', controller.getVisitor);
//POST /write : 방명록 하나 작성
router.post('/write', controller.postVisitor);
//PATCH /update 방명록 하나 수정
router.patch('/update', controller.patchVisitor);
// //DELETE /delete 방명록 하나 삭제
router.delete('/delete', controller.deleteVisitor);

module.exports = router;

controller/Cpage.js

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

exports.pageVisitor = (req, res) => {
    res.render('visitor');
};

-> 페이지를 열어주는 역할이다.

controller/Cvisitor.js

  • 비동기인 async, await로 모두 만들었다.
const Visitor = require('../model/Visitor');

//전체 방명록 조회
const allVisitor = async (req, res) => {
    const response = await Visitor.allVisitor(); //db에 모든 값 보여주기 실행됌
    console.log(response);
    res.json({ result: response });
};

//하나 방명록 조회
const getVisitor = async (req, res) => {
    console.log(req.params.id); //id를 가져오는 법
    const data = await Visitor.getVisitor(req.params.id); //model에 있는 getVisitor
    console.log(data);
    res.json({ result: data }); //data보내기
};

//방명록 하나씩 작성한걸 보내줘야함
const postVisitor = async (req, res) => {
    console.log(req.body);
    const data = await Visitor.postVisitor(req.body); //입력된 data
    console.log(data);
    res.json({ result: data, id: data.insertId, name: req.body.name, comment: req.body.comment });
};

//방명록 하나 수정
const patchVisitor = async (req, res) => {
    await Visitor.patchVisitor(req.body);
    res.json({ result: true });
};

//방명록 하나 삭제
const deleteVisitor = async (req, res) => {
    await Visitor.deleteVisitor(req.body.id);
    res.json({ result: true });
};
module.exports = { allVisitor, getVisitor, postVisitor, patchVisitor, deleteVisitor };

views/404.ejs

    <body>
        <h1>PAGE NOT FOUND</h1>
    </body>
</html>

views/index.ejs

    <body>
        <h1>MVC 패턴과 MySQL 연결</h1>
        <h4>메인페이지</h4>
        <a href="/visitor">방명록 남기기</a>
    </body>
</html>

views/visitor.ejs

  • axios 를 쓸 것이기 때문에 cdn 을 추가해야한다.
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
 <h1>방명록</h1>
        <form name="visitor-form">
            <fieldset>
                <legend>방명록 등록</legend>
                <input type="text" id="name" placeholder="방문자 이름" /><br />
                <input type="text" id="comment" placeholder="방명록 작성" /><br />
                <button type="button" onclick="registerFunc()">등록</button>
            </fieldset>
        </form>
        <br />
        <table border="1" cellspacing="0">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>작성자</th>
                    <th>내용</th>
                    <th>수정</th>
                    <th>삭제</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>

➡️ 내용이 들어갈 테이블을 만들고, 버튼을 클릭하면 바로 테이블에 추가되도록 만들어야한다.

이 이후로는 <script></script>에 들어갈 내용이다.

<script>
            const tbody = document.querySelector('tbody');
            //js가 열리면서 여기에 있는걸 가장 먼저 보이게한다.  document.ready() 랑 똑같은 역할
            (async function () {
                const res = await axios({
                    method: 'get',
                    url: '/api/visitor',
                });
                console.log(res.data.result);
                for (let i = 0; i < res.data.result.length; i++) {
                    const html = `
                    <tr>
                        <td>${res.data.result[i].id}</td>
                        <td>${res.data.result[i].name}</td>
                        <td>${res.data.result[i].comment}</td>
                        <td><button type = "button" onclick = "editFunc(${res.data.result[i].id})">수정</button></td>
                        <td><button type = "button" onclick = "deleteFunc(${res.data.result[i].id})">삭제</button></td>
                    </tr>
                    `;
                    tbody.insertAdjacentHTML('beforeend', html); //html을 넣는 것.
                }
            })();

</script> 

➡️ 테이블 내용이 실시간으로 변동하는 것을 반영하기 위해선 벡틱을 이용해야한다.

📌 [참고] insertAdjacentHTML()

위의 코드에서 쓰인 insertAdjacentHTML()지정된 텍스트를 HTML 혹은 XML로 파싱하고 결과 노드들을 지정된 위치의 DOM 트리에 삽입 하는 역할을 한다.

insertAdjacentHTML(position, text) //이 방식으로 쓰인다.

🌟 매개변수
position
요소와 상대적인 위치를 나타내는 문자열이다. 다음의 값들 중 하나여야한다.

  • beforebegin
    요소 이전에 위치. 오직 요소가 DOM 트리에 있고 부모 요소를 가지고 있을 때만 유효하다.

  • afterbegin
    요소 바로 안에서 처음 자식 이전에 위치한다.

  • beforeend
    요소 바로 안에서 마지막 자식 이후에 위치한다.

  • afterend
    요소 이후에 위치. 오직 요소가 DOM 트리에 있고 부모 요소를 가지고 있을 때만 유효하다.

text
HTML 혹은 XML로 파싱되고 트리에 삽입되는 문자열이다.

registerFunc() 부분

<script>
 async function registerFunc() {
                const form = document.forms['visitor-form'];
                const res = await axios({
                    //db의 실제주소(백엔드주소)로 데이터보내서 db에 넣음
                    method: 'post',
                    url: '/api/visitor/write',
                    data: {
                        name: form.name.value,
                        comment: form.comment.value,
                    },
                });
                const html = `
                    <tr>
                        <td>${res.data.id}</td>
                        <td>${res.data.name}</td>
                        <td>${res.data.comment}</td>
                        <td><button type = "button" onclick = "editFunc(${res.data.id})">수정</button></td>
                        <td><button type = "button" onclick = "deleteFunc(${res.data.id})">삭제</button></td>
                    </tr>
                    `;
                tbody.insertAdjacentHTML('beforeend', html); //html을 넣는 것.
            }
</script>

➡️ 등록을 누르면 반복되면서 테이블이 생성되는 것은 동일하다.
➡️ /api/visitor/write 사이트로 변경되면서 등록했을때 action이 진행되는 것.

editFunc() 부분

<script>
async function editFunc(id) {
                const form = document.forms['visitor-form'];
                const res = await axios({
                    //db의 실제주소(백엔드주소)로 데이터보내서 db에 넣음
                    method: 'patch',
                    url: '/api/visitor/update',
                    data: {
                        name: form.name.value,
                        comment: form.comment.value,
                        id: Number(id),
                    },
                });
                if (res.data.result) {
                    document.location.reload(); //true가 되면 새로고침하면서 자동으로 반영되게 된다.
                }
            }
</script>

➡️ 수정 버튼을 눌렀을 때 reload가 되면서 바로 수정된 값이 반영되게 하였다.
➡️ if문의 기본값은 true이므로 해당 데이터의 값이 전달되면 바로 그 값을 반영하게 보여준다.
➡️ 매개변수로 id 하나만 이용해도 수정 및 삭제하는데 문제 없으므로 하나만 이용

deleteFunc() 부분

<script>
  async function deleteFunc(id) {
                if (!confirm('삭제하시겠습니까?')) {
                    //confirm하고 메시지 넣으면 작동실행전에 alert 뜨는 것과 같음
                    console.log('취소');
                    return; // == 취소라는 의미, 취소버튼 누르면 아예 빠져나가 버리게.
                    //그냥 confirm하게 되면 확인이니까 저거 하나만 작성하면 되니까
                }
                const res = await axios({
                    method: 'delete',
                    url: '/api/visitor/delete',
                    data: {
                        id: Number(id),
                    },
                });
                if (res.data.result) {
                    document.location.reload();
                }
            }

</script>

➡️ 삭제는 한번 더 되물어보는 confirm()메서드를 이용하는 것이 좋다.
alert와 같이 경고문을 띄우는 것이다.
➡️ confirm 메서드는 확인을 누르면 true, 취소는 false를 반환한다. 이때 ! 추가한 것은 취소를 눌렀을 경우, 아무것도 없는 것을 반환(return)하면 현상태를 유지하게 만들기 위해서이다.

다음은 MySQL을 연결해야한다. 일단 workbench를 이용해서 DB를 만들었다.

➡️ 웹에서 등록, 수정 및 삭제를 해도 실시간으로 반영된다.

model/Visitor.js

//mysql로 db가져오기2
const mysql = require('mysql2/promise'); //모듈 가져옴
const { get } = require('../routes/visitor');
//mysql 연결
//host : 주소
// 그 밑에는 가지고 올 db의 주소들이다.
const getConn = async () => {
    return await mysql.createConnection({
        host: 'localhost',
        user: 'minjeong',
        password: process.env.DB_PASSWORD,
        database: 'kdt',
        port: 3306,
    });
};

//쿼리문 작성
//callback함수 들어가야함
//사용자가 있는 모든 데이터 : allVisitor
const allVisitor = async () => {
    //연결에 대한 자동화 처리를 해주지 못함 , 직접 연결해주고 종료해줘야함
    const conn = await getConn();
    const query = 'SELECT * FROM visitor';
    const [rows] = await conn.query(query);
    await conn.end();
    return rows;
};

const getVisitor = async (id) => {
    const conn = await getConn();
    const query = 'SELECT * FROM visitor WHERE id = ?';
    const [rows] = await conn.query(query, [id]); //쿼리문 실행 []로 가져오는건 data가 배열 형태로 가져와지므로, id를 배열형태로 가져오는 이유는 여러개 들어갈 수 있기 때문이다.
    await conn.end();
    return rows;
};

//data는 객체형태로 올 것
const postVisitor = async (data) => {
    const conn = await getConn(); //sql 연결
    //insert는 성공실패 여부만 알려줌
    const query = `INSERT INTO visitor (name,comment) VALUES (?,?)`;
    const [result] = await conn.query(query, [data.name, data.comment]);
    await conn.end();
    return result;
};

const patchVisitor = async (data) => {
    const conn = await getConn();
    const query = `UPDATE visitor SET name = ?, comment = ? WHERE id = ?`;
    const [result] = await conn.query(query, [data.name, data.comment, data.id]);
    await conn.end();
    return result;
};

const deleteVisitor = async (id) => {
    const conn = await getConn();
    const query = `DELETE FROM visitor WHERE id = ?`;
    const [result] = await conn.query(query, [id]);
    await conn.end();
    return result;
};
module.exports = { allVisitor, getVisitor, postVisitor, patchVisitor, deleteVisitor };

문자열 보간법

  • model/Visitor.js 에서 보간법을 썼다.
  • 보간법을 쓰지 않으면 해킹의 위험이 매우 높아지고, sql 인젝션 공격에 취약하며, 문자열에 특수문자가 포함될 경우 오류가 발생할 수도 있기 때문에 보간법 사용을 지향해야한다.
  • 문자열을 ?로 변경만 해주고, 값을 받아올때 순서대로 배열로 작성해주면 된다.
//1. 보간법을 사용 x
const query = `UPDATE visitor SET name = '${data.name}', comment = '${data.comment}' WHERE id = ${data.id}`;
//2. 보간법 사용 o
const query = `UPDATE visitor SET name = ?, comment = ? WHERE id = ?`;
const [result] = await conn.query(query, [data.name, data.comment, data.id]);

라우터 정리

(1) GET / : main 페이지 보이기(index.js)
(2) GET /visitor : 방명록 전체 보이기(visitor.ejs)
(3) GET /visitor/get : 방명록 하나 조회
(4) POST /visitor/write : 방명록 하나 추가
(5) PATCH /visitor/edit : 방명록 하나 수정
(6) DELETE /visitor/delete : 방명록 하나 삭제

결과


마무리

새로운 내용을 정말 많이 배웠다. MVC구조로 폴더를 구분하고 파일생성하는 것이 아직은 익숙치 않으면 정말 필요한 과정이라는 것을 느꼈다. SQL을 처음 연결해봤는데 새삼 재미있었다. 이전에는 실시간 반영이라는 것은 꿈도 못 꿨는데 DB에 자동으로 등록되고 지워지는 것을 배운 지금은 이전보단 쉽게 만들 수 있을 것 같다.

profile
중요한 건 꺾여도 다시 일어서는 마음

0개의 댓글