Sequelize 시리즈


2019. 07. 21 수정

 

이전 글에서 sequelize에 대한 기본 개념을 알아보았습니다.

이번 글에서는 sequelize를 이용하여 게시판을 만들어보겠습니다.

  • 개발환경
    • express-generator 4.16.1
    • MySQL 8.0.16
    • sequelize 5.10.1

 

 

 

 

1. 준비 작업 - 프로젝트 생성 ~ 모델 정의

express-generator로 프로젝트를 생성하는 것부터 sequelize로 모델을 정의하는 부분까지 준비 작업을 진행하겠습니다.

 

1) 프로젝트 생성 및 npm 설치

# express -e seq-crud-exam
# cd seq-crud-exam
# npm install
# npm install mysql2 sequelize
# npm install -g sequelize-cli

 

2) sequelize 빌드

# sequelize init

sequelize init으로 생성된 /config/config.json 파일에서 DB 환경을 설정합니다. ( 참고 )

{

...
    "development": {
    "username": "root",
        "password": "비밀번호를 입력해주세요.",
        "database": "접근할 데이터베이스를 입력해주세요.",
        "host": "127.0.0.1",
        "dialect": "mysql",
        "operatorsAliases": false
},
...
}

 

 

3) sync() 작성

app.js 파일에서 DB 연결을 위한 sync() 메서드를 작성합니다.

...
const models = require("./models/index.js");

models.sequelize.sync().then( () => {
  console.log(" DB 연결 성공");
}).catch(err => {
  console.log("연결 실패");
  console.log(err);
});
...

 

4) 모델 생성

sequelize-cli로 post 모델을 생성합니다. ( 참고 )

# sequelize model:create --name post --attributes "title:string, writer:string"
  • post 테이블
    • title 컬럼
    • writer 컬럼

 

sequelize-cli로 생성된 /models/post.js 파일에서 컬럼에 제약조건을 추가합니다.

'use strict';
module.exports = (sequelize, DataTypes) => {
  var post = sequelize.define('post', {
    title: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    writer: {
      type: DataTypes.STRING,
      allowNull: false,
    }
  });
  return post;
};

 

그리고 /migrations/20190720033800-create-post.js 파일에서도 모델 정의를 일치시킵니다.

'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('posts', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      title: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      writer: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('posts');
  }
};

 

마지막으로 모델 정의를 실제 DB에 반영하기 위해, migrate 명령어를 실행하고, 서버를 실행합니다.

# sequelize db:migrate

# npm start

 

테이블이 잘 생성되었는지 확인합니다.

# desc posts;

 

 

지금까지 프로젝트 생성부터 post 모델을 정의하여 migration까지 진행해보았습니다.

 

 

 

 

2. 뷰 페이지 생성 및 라우터 등록

이제 본격적으로 post 테이블에 sequelize로 데이터를 조작하여, 게시판을 만들어보겠습니다.

우선 뷰 페이지를 만들어 보겠습니다.

views 폴더에 show.ejs 파일을 생성합니다.

이 페이지는 게시글을 작성하면서, 게시글들을 볼 수 있는 페이지입니다.

 

/views/show.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<h1>목록 추가하기</h1>
<hr>

<form action="/board" method="POST">
    <table>
        <tr>
            <td><input type="text" name="inputTitle" placeholder="제목을 입력하세요."></td>
        </tr>
        <tr>
            <td><input type="text" name="inputWriter" placeholder="작성자를 입력하세요."></td>
        </tr>
    </table>
    <input type="submit" value="전송하기">
</form>
</body>
</html>

 

그리고 라우터 /routes/index.js 에서 뷰 페이지를 보여주기 위한 경로를 등록합니다.

var express = require('express');
var router = express.Router();

router.get('/board', function(req, res, next) {
res.render('show');
});

module.exports = router;

 

이제 서버를 실행하여, 브라우저에서 http://localhost:3000/board 으로 접근한 후, 뷰 페이지가 잘 보이는지 확인해보세요.

 

 

 

 

 

3. 데이터 추가하기 - INSERT

이제 뷰 페이지에서 작성한 form의 action 경로( /board )로 요청이 들어오면,

라우터에서 사용자가 입력했던 데이터를 받아 sequelize로 DB에 추가해보겠습니다.

 

/routes/index.js

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

router.get('/board', function(req, res, next) {
    res.render('show');
});

router.post('/board', function(req, res, next) {
    let body = req.body;

    models.post.create({
        title: body.inputTitle,
        writer: body.inputWriter
    })
        .then( result => {
            console.log("데이터 추가 완료");
            res.redirect("/board");
        })
        .catch( err => {
            console.log("데이터 추가 실패");
        })
});

module.exports = router;
  • sequelize를 사용하는 궁극적인 목표는 query를 직접 작성하지 않고 직관적인 이름을 가진 메서드를 사용함으로써 query를 대체하는 것입니다.
  • create() 메서드는 sequelize에서 제공하는 메서드이며, 내부적으로 INSERT 쿼리가 실행됩니다.
    • create() 메서드의 인자로는 테이블에 추가할 데이터들을 객체로 정의하여 전달합니다.
    • 예제에서는 title과 writer 컬럼에 데이터를 추가해야 하므로, 이들을 key로 갖는 객체를 전달했습니다.

sequelize는 Promise 문법이 내부적으로 동작한다고 했었습니다.

따라서 query가 잘 수행되었으면 then() 메서드가 수행되어 콜백함수가 실행됩니다.

 

http://localhost:3000/board 에서 실제로 데이터를 추가해보고, 정말 데이터가 추가되었는지 확인해보세요.

 

 

 

 

 

4. 데이터 조회하기 - SELECT

이번에는 posts 테이블에 있는 데이터들을 뿌려주도록 하겠습니다.

 

게시글을 등록하는 show.ejs 파일에서 데이터들을 출력하는 <table> 요소를 추가합니다.

/views/show.ejs

<table>
    <tr>
        <td>제목</td>
        <td>작성자</td>
        <td>작성일</td>
    </tr>
    <% for(let post of posts) { %>
        <tr>
            <td><%= post.title %></td>
            <td><%= post.writer %></td>
            <td><%= post.createdAt %></td>
        </tr>
    <% } %>
</table>
  • post는 라우터에서 렌더링할 때 넘겨주는 변수입니다.

 

다음으로 데이터를 뿌려주기 위해 /board경로에 GET으로 요청되는 라우터 미들웨어를 수정합니다.

/routes/index.js

router.get('/board', function(req, res, next) {
    models.post.findAll().then( result => {
        res.render("show", {
            posts: result
        });
    });
});

posts 테이블의 모든 데이터를 출력하기 위해 findAll() 메서드를 호출했습니다.

예제에서는 인자로 아무것도 전달하지 않았지만, 인자로 where 조건을 전달할 수 있습니다.

router.get('/board', function(req, res, next) {
  models.post.findAll({
    where: {writer: "victolee"}
  })
  .then( result => {
    res.render("show", {
      posts: result
    });
  })
  .catch(function(err){
    console.log(err);
  });
});

이렇게 조건을 추가하여, 특정 작성자에 대한 목록만 불러올 수 있습니다.

 

where 대신 attributes, order 등의 조건을 추가할 수 있습니다. ( 참고 )

  • attributes: ["title"]
    • title 컬럼만 조회
  • order: [["id", "DESC"]]
    • id를 내림차순으로 정렬

 

이제 http://localhost:3000/board 에서 게시글들이 잘 조회되는지 확인해보세요.

 

 

 

 

5. 데이터 업데이트 - UPDATE

다음으로 게시글을 수정하기 위한 뷰 페이지와 라우터 함수를 등록하겠습니다.

 

먼저 수정하기 버튼이 있어야 합니다.

게시글 삭제도 진행해야 하므로 "수정하기", "삭제하기" 버튼을 한꺼번에 추가하도록 하겠습니다.

 

/views/show.ejs  - <table> 부분

<table>
    <tr>
        <td>제목</td>
        <td>작성자</td>
        <td>작성일</td>
    </tr>
    <% for(let post of posts) { %>
        <tr>
            <td><%= post.title %></td>
            <td><%= post.writer %></td>
            <td><%= post.createdAt %></td>
            <td><button ><a href="/board/<%=post.id%>">수정하기</a></button></td>
            
            <form action="/board/<%=post.id%>?_method=DELETE" method="post">
                <td><input type="submit" value="삭제하기"></input></td>
            </form>
        </tr>
    <% } %>
</table>
  • <%= post.id %>
    • 수정, 삭제 버튼을 클릭할 때 게시글의 id 값을 path variable로 넘겨줍니다.
    • 그 이유는 수정을 하기 위해서 기존의 정보들( 제목, 작성자 )이 그대로 나타나야 하기 때문에, posts 테이블의 PK인 id 값을 넘겨준 것입니다.
    • 반복문을 수행했기 때문에 각 게시글마다 <%= post.id %>값이 다르기 때문에, 각 버튼의 경로가 다르게 됩니다.

 

다음으로 라우터에서 기존에 작성된 게시글의 정보를 반환하는 코드를 작성합니다.

/rotues/index.js

router.get('/edit/:id', function(req, res, next) {
  let postID = req.params.id;

  models.post.findOne({
    where: {id: postID}
  })
  .then( result => {
    res.render("edit", {
      post: result
    });
  })
  .catch( err => {
    console.log("데이터 조회 실패");
  });
});
  • 라우터 함수 작성시 경로는 /edit/:id 입니다.
    • 예를 들어, 만약 라우터 함수의 경로를 /edit/:foo 로 작성했다면, req.params.foo가 될 것입니다.
    • "수정하기" 버튼을 눌렀을 때 id 값이 다르기 때문에, 동적으로 처리하기 위해 id 값을 파라미터로 받아야 합니다.
    • req.params 프로퍼티를 통해 path variables 값을 얻을 수 있습니다.
  • 여기서 findOne() 메서드를 호출했는데, findOne() 메서드는 findAll()과 달리 1개의 row만 조회합니다.
    • 참고) 기존에 find() 메서드를 사용했었는데.... Sequelize5부터 find() 메서드는 제거되었네요. ( 참고 )
  • 마지막으로 조회된 게시글 정보를 post 변수에 담아 /views/edit.ejs 페이지로 넘겨줍니다.

 

 

이제 /views/edit.ejs 파일을 생성하여, 수정할 데이터를 입력하는 뷰 페이지를 작성합니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<h1>목록 추가하기</h1>
<hr>

<form action="/board/<%= post.id %>?_method=PUT" method="post">
    <table>
        <tr>
            <td><input type="text" name="editTitle" value="<%= post.title %>"></td>
        </tr>
        <tr>
            <td><input type="text" name="editWriter" value="<%= post.writer %>"></td>
        </tr>
    </table>
    <input type="submit" value="전송하기">
</form>
</body>
</html>

기존 게시글의 정보들을 출력해주기 위해 각 input 태그 마다 value 값을 지정해주었습니다.

 

RESTful 방식을 따르기 위해서는 게시글 "수정"이므로, POST 방식이 아니라 PUT 방식으로 요청을 보내야 합니다.

form 태그는 GET과 POST 방식밖에 지원하지 않으므로, Node.js에서 form에 PUT, DELETE 방식으로 데이터를 전달하려면 모듈이 필요합니다.

 

그 모듈은 method-override이며, 깃헙 페이지( 링크 )에서 확인하실 수 있습니다.

깃헙에서 HTTP Method를 오버라이드하는 방식을 여러가지로 소개하고 있는데, 저는 override using a query value 방식을 사용했습니다.

 

이 모듈을 사용하기 위해서는 npm을 설치하고 app.js에서 미들웨어로 등록해야 합니다.

# npm install method-override
...
const methodOverride = require('method-override');
...
app.use(methodOverride('_method'));
...

 

마지막으로 수정된 데이터를 DB에 Update 하는 부분을 작성하겠습니다.

/views/edit.ejs 에서 글을 수정하면 update 경로로 요청을 보내는데, 이 때도 역시 path variable로 id 값을 전달합니다.

 

/routes/index.js

router.put('/board/:id', function(req, res, next) {
    let postID = req.params.id;
    let body = req.body;

    models.post.update({
        title: body.editTitle,
        writer: body.editWriter
    },{
        where: {id: postID}
    })
        .then( result => {
            console.log("데이터 수정 완료");
            res.redirect("/board");
        })
        .catch( err => {
            console.log("데이터 수정 실패");
        });
});
  • 업데이트를 하기 위해서 update()메서드를 호출합니다.
    • 주의할 것은 where 조건을 작성하지 않으면 posts 테이블의 모든 데이터가 삭제됩니다.
    • 꼭 where를 작성하여 특정 게시글만 삭제하도록 주의해주세요.
    • 첫 번째 인자는 수정할 필드와 값을 정의해주고, ( create 할 때 처럼 )
    • 두 번째 인자는 어떤 게시글에 수정을 할지 명시해줘야 합니다.

정말로 수정이 잘 되는지 확인해보도록 합니다.

 

 

 

 

6. 데이터 삭제하기

CRUD의 마지막 "삭제하기"만 남았습니다.

 

/views/index.js 파일에서 삭제하기 버튼을 아래와 같이 작성을 했었습니다.

<form action="/board/<%=post.id%>?_method=DELETE" method="post">
    <td><input type="submit" value="삭제하기"></input></td>
</form>
  • method-override 하여, DELETE 방식으로 요청하기 위해 삭제하기 버튼을 form 태그로 감쌌습니다.
  • 삭제하기도 수정하기와 마찬가지로 특정 게시글을 식별하기 위한 PK 값을 서버로 보내기 위해 path variable로 id 값을 전달했습니다.

 

마지막으로 게시글 삭제를 하는 라우터 함수를 작성하겠습니다.

/routes/index.js

router.delete('/board/:id', function(req, res, next) {
  let postID = req.params.id;

  models.post.destroy({
    where: {id: postID}
  })
  .then( result => {
    res.redirect("/board")
  })
  .catch( err => {
    console.log("데이터 삭제 실패");
  });
});
  • delete() 메서드를 호출하여 HTTP delete 메서드를을 받을 수 있습니다.
  • sequelize에서는 destroy() 메서드를 호출하여 데이터를 삭제 할 수 있습니다.
    • 주의할 것은 where 조건을 작성하지 않으면 posts 테이블의 모든 데이터가 삭제됩니다.
    • 꼭 where를 작성하여 특정 게시글만 삭제하도록 주의해주세요.

마지막으로 삭제가 잘 되는지도 테스트합니다.

 

 

 

 

 

7. 매개변수 받는방법 정리

추가적으로 클라이언트가 보낸 데이터를 라우터 미들웨어에서 받는 방법에 대해 정리하려고 합니다.

 

  1. URL 파라미터
    • ex) 라우터를 등록할 때 app.get( /post/:postid ) 으로 경로를 설정했다면, 클라이언트에서 "/post/3"으로 요청 시, req.params.postid == 3 입니다.
    • req.params.변수명
    • 이 방식은 게시글 수정과 삭제 할 때 사용했던 방법인데, URL에 동적인 데이터를 추가하는 방식입니다.
  2. query 파라미터
    • ex) 클라이언트에서 "/post?postid=3"으로 요청 시, req.query.postid == 3 입니다.
    • req.query.키
    • URL 파라미터 방식과 같이 URL을 통해서 데이터를 얻을 수 있지만, query string의 key 값을 받아올 수 있습니다.
  3. form 파라미터
    • ex) 클라이언트에서 input 태그의 name="title" 일 때, "victolee"를 입력하고 전송 버튼을 누르면 req.body.title == vcitolee 입니다.
    • req.body.name이름
    • HTML form 태그를 통해 넘어온 데이터를 받는 방법입니다.

 

 

 

 

8. 여러 개의 데이터 추가하기

이 글의 예제에서는 create() 메서드의 매개변수로 객체를 넘겨줌으로써, 테이블에 하나의 row만 추가할 수 있었습니다.

그런데 여러 개의 데이터를 추가하고 싶을 땐 어떻게 해야 할까요?

 

다행히도 sequelize에서 대량의 데이터를 추가할 수 있도록 bulkCreate()메서드를 지원하고 있습니다. ( 참고 )

 

소개만 할 목적으로 문서에서 예제를 가져왔습니다.

 

 

bulkCreate() 메서드의 인자로는 배열을 넘겨줍니다.

그리고 배열의 원소는 객체가 담겨있고, 객체의 프로퍼티는 테이블의 필드가 정의되어 있습니다.

 

 

 

이상으로 Sequelize를 사용하여 CRUD 기능이 있는 Restful 게시판 애플리케이션을 만들어보았습니다.

다음 글에서는 sequelize로 모델의 관계를 맺는 것과 이를 활용하여 게시글의 댓글을 작성 해보도록 하겠습니다.