본문 바로가기

ComputerScience/NodeJs

node - 7.6 시퀄라이즈 사용하기

728x90
 

Node.js 교과서 개정 2판: 7장 MySQL - 1

 

thebook.io

* 위 내용을 정리하였음

7.6 시퀄라이즈 사용하기

- 노드에서 MySQL 데이터 베이스를 작업하기 쉽게 해주는 라이브러리 이다.

- ORM(Object-relational Mapping)이라고 한다. 자바스크립트 객체와 데이터베이스의 relation을 매핑해주는 도구이다.

- 즉 자바스크립트 구문을을 알아서 SQL로 바꿔준다.

$ npm i sequelize sequelize-cli mysql2

- sequelize-cli : 시퀄라이즈 명령어를 실행하기 위한 패키지

- mysql2 : MySQL과 시퀄라이즈를 이어주는 드라이버

- 설치 완료 후 sequelize init 명령어를 호출한다. 전역 설치 없이 명령어로 사용하려고 앞에 npx를 붙였다.

$ npx sequelize init
Sequelize CLI [Node: 14.0.0, CLI: 6.1.0, ORM: 6.2.3]
Created "config\config.json"
Successfully created models folder at ...
Successfully created migrations folder at ...
Successfully created seeders folder at ...

- config, models, migrations, seeders 폴더가 생성되었다.

- models 폴더 안에 index.js를 다음과 같이 수정한다.

const Sequelize = require('sequelize');

const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const db = {};

const sequelize = new Sequelize(config.database, config.username, config.password, config);
db.sequelize = sequelize;

module.exports = db;

- Sequelize는 시퀄라이즈 패키지이자 생성자이다. config/config.json에서 데이터베이스 설정을 불러온 후 new Sequelize를 통해 MySQL 연결 객체를 생성한다.

- config설정들은 process.env.NODE_ENV development일 때 적용된다.

- 나중에 배포할 때 process.env.NODE_ENV production으로 설정한다.

- 즉 배포 환경을 위해 데이터베이스를 설정할 때는 config/config.json의 production 속성을 수정하면 된다. 

- 연결 객체를 나중에 재사용하기 위해 db.sequelize에 넣어두었다.

 

7.6.1 MySQL 연결하기

- app.js를 만들고 아래 코드를 작성한다.

const express = require('express');
const path = require('path');
const morgan = require('morgan');
const nunjucks = require('nunjucks');

const { sequelize } = require('./models');

const app = express();
app.set('port', process.env.PORT || 3001);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});
sequelize.sync({ force: false })
  .then(() => {
    console.log('데이터베이스 연결 성공');
  })
  .catch((err) => {
    console.error(err);
  });

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use((req, res, next) => {
  const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기 중');
});

- sequelize를 불러올때 require('./models') require('./models/index.js')와 동일하다. (index.js는 생략 가능)

- db.sequelize를 불러와서 sync 메서드를 사용해 서버 실행 시 MySQL과 연동되도록 한다. force: false 옵션을 true로 설정하면 서버 실행 시마다 테이블을 재생성한다.

 

- MySQL과 연동할 때는 config/config.json 정보가 사용된다. operatorAliases 속성이 들어 있다면 삭제한다.

{
  "development": {
    "username": "root",
    "password": "[root 비밀번호]",
    "database": "nodejs",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
...
}

- development.password development.database를 현재 MySQL 커넥션과 일치하게 수정한다. 

- test production은 각각 테스트 용도와 배포 용도로 접속하기 위해 사용되는 것이다.

- npm start로 앱을 실행해보자.

 

 

7.6.2 모델 정의하기

- MySQL로 정의한 테이블들을 sequelize에도 정의해준다.

- 서로 대응되도록 정의한 다음 MySQL의 테이블들을 연결한다.

- models/user.js파일을 만들어 아래 코드를 작성한다.

const Sequelize = require('sequelize');

module.exports = class User extends Sequelize.Model {
  static init(sequelize) {
    return super.init({
      name: {
        type: Sequelize.STRING(20),
        allowNull: false,
        unique: true,
      },
      age: {
        type: Sequelize.INTEGER.UNSIGNED,
        allowNull: false,
      },
      married: {
        type: Sequelize.BOOLEAN,
        allowNull: false,
      },
      comment: {
        type: Sequelize.TEXT,
        allowNull: true,
      },
      created_at: {
        type: Sequelize.DATE,
        allowNull: false,
        defaultValue: Sequelize.NOW,
      },
    }, {
      sequelize,
      timestamps: false,
      underscored: false,
      modelName: 'User',
      tableName: 'users',
      paranoid: false,
      charset: 'utf8',
      collate: 'utf8_general_ci',
    });
  }
  static associate(db) {
    db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id' });
  }
};

- User 모델은 Sequelize.Model을 확장한 클래스이다.

- 모델은 크게 static init 메서드와 static associate 메서드로 나뉜다.

- static init 메서드에는 테이블에 대한 설정을 한다.

- static associate 메서드에는 다른 모델과의 관계를 적는다.

- init 메서드에서 super.init 메서드의 첫 번째 인수가 테이블 컬럼에 대한 설정이고, 두 번째 인수가 테이블 자체에 대한 설정이다.

- 시퀄라이즈는 알아서 id를 Primary key로 연결하므로 id 컬럼은 적어줄 필요가 없다. 나머지 컬럼의 스펙을 입력한다. MySQL 테이블과 컬럼 내용이 일치해야 정확하게 대응된다.

 

▼ 표 7-1 MySQL과 시퀄라이즈의 비교

MySQL 시퀄라이즈
VARCHAR(100) STRING(100)
INT INTEGER
TINYINT BOOLEAN
DATETIME DATE
INT UNSIGNED INTEGER.UNSIGNED
NOT NULL allowNull: false
UNIQUE unique: true
DEFAULT now() defaultValue: Sequelize.NOW

 

- super.init 메서드의 두 번째 인수는 테이블 옵션이다.

 sequelize: static init 메서드의 매개변수와 연결되는 옵션으로 db.sequelize 객체를 넣어야 한다. 나중에 model/index.js에서 sequelize를 매개변수로 넘겨줘서 연결한다.

 timestamps: 현재 false로 되어 있으며, 이 속성 값이 true면 시퀄라이즈는 createdAt updatedAt 컬럼을 추가한다. 각각 로우가 생성될 때와 수정될 때의 시간이 자동으로 입력된다.

 underscored: 시퀄라이즈는 기본적으로 테이블명과 컬럼명을 캐멀 케이스(camel case)(예시: createdAt)로 만든다. 이를 스네이크 케이스(snake case)(예시: created_at)로 바꾸는 옵션이다.

 modelName: 모델 이름을 설정할 수 있다. 노드 프로젝트에서 사용한다. (보통 모델 이름은 단수형, 테이블 이름은 복수형으로 사용한다.)

 tableName: 실제 데이터베이스의 테이블 이름이다. 기본적으로는 모델 이름을 소문자 및 복수형으로 만든다. 모델 이름이 User라면 테이블 이름은 users가 된다.

 paranoid: true로 설정하면 deletedAt이라는 컬럼이 생긴다. 로우를 삭제할 때 완전히 지워지지 않고 deletedAt에 지운 시각이 기록된다. 로우를 조회하는 명령을 내렸을 때는 deletedAt의 값이 null인 로우(삭제되지 않았다는 뜻)를 조회한다 . 이렇게 하는 이유는 나중에 로우를 복원하기 위해서다. 로우를 복원해야 하는 상황이 생길 것 같다면 미리 true로 설정한다.

 charset collate: 각각 utf8 utf8_general_ci로 설정해야 한글이 입력된다. 이모티콘까지 입력할 수 있게 하고 싶다면 utf8mb4 utf8mb4_general_ci를 입력한다.

 

- 이번에는 models/comment.js를 만들어보자.

const Sequelize = require('sequelize');

module.exports = class Comment extends Sequelize.Model {
  static init(sequelize) {
    return super.init({
      comment: {
        type: Sequelize.STRING(100),
        allowNull: false,
      },
      created_at: {
        type: Sequelize.DATE,
        allowNull: true,
        defaultValue: Sequelize.NOW,
      },
    }, {
      sequelize,
      timestamps: false,
      modelName: 'Comment',
      tableName: 'comments',
      paranoid: false,
      charset: 'utf8mb4',
      collate: 'utf8mb4_general_ci',
    });
  }

  static associate(db) {
    db.Comment.belongsTo(db.User, { foreignKey: 'commenter', targetKey: 'id' });
  }
};

- 여기에는 commenter 컬럼이 없다. users테이블로의 외래키인데 comment모델을 정의할때 넣어도 되지만 다음 절에서 관계를 정의할때 넣어보자.

 

- 이제 만든 모델들을 index.js와 연결해보자.

const Sequelize = require('sequelize');
const User = require('./user');
const Comment = require('./comment');

const env = process.env.NODE_ENV || 'development';
const config = require('../config/config')[env];
const db = {};

const sequelize = new Sequelize(config.database, config.username, config.password, config);
db.sequelize = sequelize;
db.User = User;
db.Comment = Comment;

User.init(sequelize);
Comment.init(sequelize);

User.associate(db);
Comment.associate(db);

module.exports = db;

- db라는 객체에 User와 Comment 모델을 담아두었습니다.

- 앞으로 db 객체를 require하여 User와 Comment 모델에 접근할 것이다.

- User.init Comment.init은 각각의 모델의 static.init 메서드를 호출하는 것이다. init이 실행되어야 테이블이 모델로 연결된다.

- 다른 테이블과의 관계를 연결하는 associate 메서드도 미리 실행해둔다.

 

7.6.3 관계 정의하기

- user 테이블과 comment 테이블 사이의 관계는 1대다 관계이다.

- 시퀄라이즈에게 이런 테이블 관계를 알려줘 보자.

 

7.6.3.1 1:N

- 1대N 관계를 시퀄라이즈에서 hasMany, belongsTo 메서드로 표현한다.

- users 테이블의 로우 하나를 불러올 때 연결된 comments의 튜플들을 불러온다.

- comments 테이블의 로우를 불러올 때 연결된 users 테이블의 튜플을 가져온다.

* 왜래키가 속성으로 추가되는 테이블이 belongTo를 호출한다.

// models/user.js

...
  static associate(db) {
    db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id' });
  }
};
// models/comment.js

...
  static associate(db) {
    db.Comment.belongsTo(db.User, { foreignKey: 'commenter', targetKey: 'id' });
  }
};

- comment 테이블은 user테이블의 id를 commenter(외래키)로하여 자신의 속성으로 갖는다.

- hasMany메서드는 소스키 id를 가지고 comment테이블의 외래키인 commenter를 통해 참조한다.

- foreignKey를 따로 지정하지 않으면 이름이 모델명+기본 키인 컬럼이 모델에 생성된다. 예를 들어 commenter foreignKey로 넣어주지 않았다면 user(모델명)+기본 키(id)가 합쳐진 UserId foreignKey로 생성된다.(commenter대신 UserId가 속성명이 된다)

- 시퀄라이즈는 테이블이 존재하지 않으면 직접 만들어준다.

 

7.6.3.2 1:1

- 1:1 관계에서는 hasOne 메서드를 사용한다.

- 사용자 정보를 담고 있는 가상의 Info 모델이 있다고 가정해보자.

db.User.hasOne(db.Info, { foreignKey: 'UserId', sourceKey: 'id' });
db.Info.belongsTo(db.User, { foreignKey: 'UserId', targetKey: 'id' });

- info 테이블에 속성으로 UserId가 추가되기 때문에 belongsTo는 User 테이블에서 사용한다.

 

7.6.3.3 N:M

- 게시글(Post 테이블)과 해시태그 모델(Hashtag 테이블) 간의 다대다(N:M) 관계를 생각해보자.

 

- N:M 관계는 belongsToMany 메서드를 사용한다.

db.Post.belongsToMany(db.Hashtag, { through: 'PostHashtag' });
db.Hashtag.belongsToMany(db.Post, { through: 'PostHashtag' });

- N:M 관계는 특성상 새로운 모델(테이블)이 생성된다

- 새로 생성된 PostHashtag 모델에는 게시글과 해시태그의 아이디가 저장된다. 

- 이렇게 만들어진 테이블은 다음처럼 접근 가능하다.

db.sequelize.models.PostHashtag

7.6.4 쿼리 알아보기

- 시퀄라이즈로 자바스크립트를 사용하여 SQL문을 생성해보자.

- 쿼리작업은 프로미스를 반환하므로 async/await 또는 then 과 함께 하용하자.

// user 테이블에 튜플을 하나 생성하는 SQL문과 시퀄라이즈 쿼리비교

INSERT INTO nodejs.users (name, age, married, comment) VALUES ('zero', 24, 0, '자기소개1');


const { User } = require('../models');
User.create({
  name: 'zero',
  age: 24,
  married: false,
  comment: '자기소개1',
});

- models 모듈에서 User 모델을 불러와 create 메서드를 사용한다.

// 로우 조회

SELECT * FROM nodejs.users;

User.findAll({});
// 튜플 하나만 조회
SELECT * FROM nodejs.users LIMIT 1;

User.findOne({});
// 원하는 속성만 가져오기

SELECT name, married FROM nodejs.users;

User.findAll({
  attributes: ['name', 'married'],
});
// where절 표현

SELECT name, age FROM nodejs.users WHERE married = 1 AND age > 30;

const { Op } = require('sequelize');
const { User } = require('../models');
User.findAll({
  attributes: ['name', 'age'],
  where: {
    married: 1,
    age: { [Op.gt]: 30 },
  },
});

- MySQL에서는 undefined라는 자료형을 지원하지 않으므로 where 옵션에는 undefined가 들어가면 안된다.

- 빈 값을 넣고자 하면 null을 대신 사용한다.

- 시퀄라이즈는 자바스크립트 객체를 사용해서 쿼리를 생성해야 하므로 Op.gt 같은 특수한 연산자들이 사용된다. 

- Sequelize 객체 내부의 Op 객체를 불러와 사용한다.

- Op.gt(초과), Op.gte(이상), Op.lt(미만), Op.lte(이하), Op.ne(같지 않음), Op.or(또는), Op.in(배열 요소 중 하나), Op.notIn(배열 요소와 모두 다름) 등이 있다.

// OP.or 사용

SELECT id, name FROM users WHERE married = 0 OR age > 30;

const { Op } = require('sequelize');
const { User } = require('../models');
User.findAll({
  attributes: ['id', 'name'],
  where: {
    [Op.or]: [{ married: 0 }, { age: { [Op.gt]: 30 } }],
  },
});
// order by 옵션

SELECT id, name FROM users ORDER BY age DESC;

User.findAll({
  attributes: ['id', 'name'],
  order: [['age', 'DESC']],
});
// 조회할 튜플 개수 제한

SELECT id, name FROM users ORDER BY age DESC LIMIT 1;

User.findAll({
  attributes: ['id', 'name'],
  order: [['age', 'DESC']],
  limit: 1,
});
// offset 속성

SELECT id, name FROM users ORDER BY age DESC LIMIT 1 OFFSET 1;

User.findAll({
  attributes: ['id', 'name'],
  order: ['age', 'DESC'],
  limit: 1,
  offset: 1,
});
// 로우 Update 쿼리

UPDATE nodejs.users SET comment = '바꿀 내용' WHERE id = 2;

User.update({
  comment: '바꿀 내용',
}, {
  where: { id: 2 },
});

 

// 로우 삭제

DELETE FROM nodejs.users WHERE id = 2;

User.destory({
  where: { id: 2 },
});

 

7.6.4.1 관계 쿼리

- 아래 처럼 튜플 하나를 꺼내서 각 컬럼들에 바로 접근할 수 있다.

const user = await User.findOne({});
console.log(user.nick); // 사용자 닉네임

- 이번에는 join을 사용하는 방법을 알아보자.

- 다음 코드는 사용자가 작성한 댓글들을 가져오는 방법이다.

- include에 참조할 테이블의 이름을 넣어준다. (배열인 이유는 여러개 일 수 있기 때문)

const user = await User.findOne({
  include: [{
    model: Comment,
  }]
});
console.log(user.Comments); // 사용자 댓글

- 아니면 다음처럼 조회할수도 있다.

const user = await User.findOne({});
const comments = await user.getComments();
console.log(comments); // 사용자 댓글

- 관계를 설정했다면 getComments(조회), setComments(수정), addComment(하나 생성), addComments(여러 개 생성), removeComments(삭제) 메서드를 지원한다.

 

- 동사 뒤의 모델 이름을 바꾸고 싶다면 관계 설정 시 as 옵션을 사용하면 된다.

// 관계를 설정할 때 as로 등록
db.User.hasMany(db.Comment, { foreignKey: 'commenter', sourceKey: 'id', as: 'Answers' });
// 쿼리할 때는
const user = await User.findOne({});
const comments = await user.getAnswers();
console.log(comments); // 사용자 댓글

- as를 설정하면 include 시 추가되는 댓글 객체도 user.Answers로 바뀐다.

- include나 관계 쿼리 메서드에도 where attributes 같은 옵션을 사용할 수 있다.

const user = await User.findOne({
  include: [{
    model: Comment,
    where: {
      id: 1,
    },
    attributes: ['id'],
  }]
});
// 또는
const comments = await user.getComments({
  where: {
    id: 1,
  },
  attributes: ['id'],
});

- 댓글을 가져올 때는 id 1인 댓글만 가져오고, 컬럼도 id 컬럼만 가져오도록 한다.

 

- 이번에는 수정, 생성, 삭제 쿼리를 살펴보자.

const user = await User.findOne({});
const comment = await Comment.create();
await user.addComment(comment);
// 또는
await user.addComment(comment.id);

- 직접 comment 튜플을 인수로 넣어도 되지만 comment.id만 인수로 넣어줘도 동일하다.

- 배열로 여러개 추가.

const user = await User.findOne({});
const comment1 = await Comment.create();
const comment2 = await Comment.create();
await user.addComment([comment1, comment2]);

 

 

7.6.4.2 SQL 쿼리하기

- 시퀄라이즈 쿼리를 사용하지 않고 SQL문을 사용할수도 있다.

const [result, metadata] = await sequelize.query('SELECT * from comments');
console.log(result);

7.6.5 쿼리 수행하기

- 모델(테이블)에서 데이터를 받아 페이지를 렌더링하는 방법과 JSON 형식으로 데이터를 가져오는 방법을 알아보자.

- 사용자 정보를 등록하고 사용자가 등록한 댓글을 가져오는 서버를 만들어보자.

- 프로젝트에 view폴더를 만들고 다음과 같이 sequelize.html 파일과 error.html 파일을 만든다.

* https://github.com/zerocho/nodejs-book에서 코드를 복사하자

// views/sequelize.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>시퀄라이즈 서버</title>
    <style>
      table { border: 1px solid black; border-collapse: collapse; }
      table th, table td { border: 1px solid black; }
    </style>
  </head>
  <body>
    <div>
      <form id="user-form">
        <fieldset>
          <legend>사용자 등록</legend>
          <div><input id="username" type="text" placeholder="이름"></div>
          <div><input id="age" type="number" placeholder="나이"></div>
          <div><input id="married" type="checkbox"><label for="married">결혼여부</label></div>
          <button type="submit">등록</button>
        </fieldset>
      </form>
    </div>
    <br>
    <table id="user-list">
      <thead>
      <tr>
        <th>아이디</th>
        <th>이름</th>
        <th>나이</th>
        <th>결혼여부</th>
      </tr>
      </thead>
      <tbody>
        {% for user in users %}
        <tr>
          <td>{{user.id}}</td>
          <td>{{user.name}}</td>
          <td>{{user.age}}</td>
          <td>{{ '기혼' if user.married else '미혼'}}</td>
        </tr>
        {% endfor %}
      </tbody>
    </table>
    <br>
    <div>
      <form id="comment-form">
        <fieldset>
          <legend>댓글 등록</legend>
          <div><input id="userid" type="text" placeholder="사용자 아이디"></div>
          <div><input id="comment" type="text" placeholder="댓글"></div>
          <button type="submit">등록</button>
        </fieldset>
      </form>
    </div>
    <br>
    <table id="comment-list">
      <thead>
      <tr>
        <th>아이디</th>
        <th>작성자</th>
        <th>댓글</th>
        <th>수정</th>
        <th>삭제</th>
      </tr>
      </thead>
      <tbody></tbody>
    </table>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="/sequelize.js"></script>
  </body>
</html>
// views/error.html

<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>

- public 폴더 안에 sequelize.js 파일도 만든다.

// public/sequelize.js

// 사용자 이름을 눌렀을 때 댓글 로딩
document.querySelectorAll('#user-list tr').forEach((el) => {
  el.addEventListener('click', function () {
    const id = el.querySelector('td').textContent;
    getComment(id);
  });
});
// 사용자 로딩
async function getUser() {
  try {
    const res = await axios.get('/users');
    const users = res.data;
    console.log(users);
    const tbody = document.querySelector('#user-list tbody');
    tbody.innerHTML = '';
    users.map(function (user) {
      const row = document.createElement('tr');
      row.addEventListener('click', () => {
        getComment(user.id);
      });
      // 로우 셀 추가
      let td = document.createElement('td');
      td.textContent = user.id;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = user.name;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = user.age;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = user.married ? '기혼' : '미혼';
      row.appendChild(td);
      tbody.appendChild(row);
    });
  } catch (err) {
    console.error(err);
  }
}
// 댓글 로딩
async function getComment(id) {
  try {
    const res = await axios.get(`/users/${id}/comments`);
    const comments = res.data;
    const tbody = document.querySelector('#comment-list tbody');
    tbody.innerHTML = '';
    comments.map(function (comment) {
      // 로우 셀 추가
      const row = document.createElement('tr');
      let td = document.createElement('td');
      td.textContent = comment.id;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = comment.User.name;
      row.appendChild(td);
      td = document.createElement('td');
      td.textContent = comment.comment;
      row.appendChild(td);
      const edit = document.createElement('button');
      edit.textContent = '수정';
      edit.addEventListener('click', async () => { // 수정 클릭 시
        const newComment = prompt('바꿀 내용을 입력하세요');
        if (!newComment) {
          return alert('내용을 반드시 입력하셔야 합니다');
        }
        try {
          await axios.patch(`/comments/${comment.id}`, { comment: newComment });
          getComment(id);
        } catch (err) {
          console.error(err);
        }
      });
      const remove = document.createElement('button');
      remove.textContent = '삭제';
      remove.addEventListener('click', async () => { // 삭제 클릭 시
        try {
          await axios.delete(`/comments/${comment.id}`);
          getComment(id);
        } catch (err) {
          console.error(err);
        }
      });
      // 버튼 추가
      td = document.createElement('td');
      td.appendChild(edit);
      row.appendChild(td);
      td = document.createElement('td');
      td.appendChild(remove);
      row.appendChild(td);
      tbody.appendChild(row);
    });
  } catch (err) {
    console.error(err);
  }
}
// 사용자 등록 시
document.getElementById('user-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const name = e.target.username.value;
  const age = e.target.age.value;
  const married = e.target.married.checked;
  if (!name) {
    return alert('이름을 입력하세요');
  }
  if (!age) {
    return alert('나이를 입력하세요');
  }
  try {
    await axios.post('/users', { name, age, married });
    getUser();
  } catch (err) {
    console.error(err);
  }
  e.target.username.value = '';
  e.target.age.value = '';
  e.target.married.checked = false;
});
// 댓글 등록 시
document.getElementById('comment-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const id = e.target.userid.value;
  const comment = e.target.comment.value;
  if (!id) {
    return alert('아이디를 입력하세요');
  }
  if (!comment) {
    return alert('댓글을 입력하세요');
  }
  try {
    await axios.post('/comments', { id, comment });
    getComment(id);
  } catch (err) {
    console.error(err);
  }
  e.target.userid.value = '';
  e.target.comment.value = '';
});

- script 태그에는 버튼들을 눌렀을 때 서버의 라우터로 AJAX 요청을 보내는 코드가 들어 있다.

- 사용할 라우터들을 미리 app.js에 연결한다.

const express = require('express');
const path = require('path');
const morgan = require('morgan');
const nunjucks = require('nunjucks');

const { sequelize } = require('./models');
const indexRouter = require('./routes');
const usersRouter = require('./routes/users');
const commentsRouter = require('./routes/comments');

const app = express();
app.set('port', process.env.PORT || 3001);
app.set('view engine', 'html');
nunjucks.configure('views', {
  express: app,
  watch: true,
});
sequelize.sync({ force: false })
  .then(() => {
    console.log('데이터베이스 연결 성공');
  })
  .catch((err) => {
    console.error(err);
  });

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/comments', commentsRouter);

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res.render('error');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 대기 중');
});

- 라우터의 내용은 다음과 같다. sequelize.js에 나오는 GET, POST, PUT, DELETE 요청에 해당하는 라우터를 만든다.

- routes 폴더를 만들고 그 안에 index.js를 작성하면 된다.

routes/index.js

const express = require('express');
const User = require('../models/user');

const router = express.Router();

router.get('/', async (req, res, next) => {
  try {
    const users = await User.findAll();
    res.render('sequelize', { users });
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

- GET /로 접속했을 때의 라우터이다. 

- User.findAll 메서드로 모든 사용자를 찾은 후, sequelize.html을 렌더링할 때 결과값인 users를 넣는다.

- 이렇게 데이터베이스에서 데이터를 조회한 후 템플릿 렌더링에 사용한다.

// routes/users.js

const express = require('express');
const User = require('../models/user');
const Comment = require('../models/comment');

const router = express.Router();

// get,post 요청의 라우트 경로가 같아서
// .route 메서드로 묶어서 표현.
router.route('/')
  .get(async (req, res, next) => {
    try {
      const users = await User.findAll();
      res.json(users);
    } catch (err) {
      console.error(err);
      next(err);
    }
  })
  .post(async (req, res, next) => {
    try {
      const user = await User.create({
        name: req.body.name,
        age: req.body.age,
        married: req.body.married,
      });
      console.log(user);
      res.status(201).json(user);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

router.get('/:id/comments', async (req, res, next) => {
  try {
    const comments = await Comment.findAll({
      include: {
        model: User,
        where: { id: req.params.id },
      },
    });
    console.log(comments);
    res.json(comments);
  } catch (err) {
    console.error(err);
    next(err);
  }
});

module.exports = router;

- GET /users POST /users 주소로 요청이 들어올 때의 라우터이다.

- GET /에서도 사용자 데이터를 조회했지만, GET /users에서는 데이터를 JSON 형식으로 반환한다.

- GET /users/:id/comments 라우터에는 findAll 메서드에 옵션이 추가되어 있다. 

- GET /users/1/comments라면 사용자 id 1인 댓글을 불러온다.

- 조회된 댓글 객체에는 include로 넣어준 사용자 정보도 들어 있으므로 작성자의 이름이나 나이 등을 조회할 수 있다.

 

// routes/comments.js

const express = require('express');
const { User, Comment } = require('../models');

const router = express.Router();

router.post('/', async (req, res, next) => {
  try {
    const comment = await Comment.create({
      commenter: req.body.id,
      comment: req.body.comment,
    });
    console.log(comment);
    res.status(201).json(comment);
  } catch (err) {
    console.error(err);
    next(err);
  }
});

router.route('/:id')
  .patch(async (req, res, next) => {
    try {
      const result = await Comment.update({
        comment: req.body.comment,
      }, {
        where: { id: req.params.id },
      });
      res.json(result);
    } catch (err) {
      console.error(err);
      next(err);
    }
  })
  .delete(async (req, res, next) => {
    try {
      const result = await Comment.destroy({ where: { id: req.params.id } });
      res.json(result);
    } catch (err) {
      console.error(err);
      next(err);
    }
  });

module.exports = router;

- POST /comments : 댓글을 생성 라우터. commenter 속성에 사용자 아이디를 넣어 사용자와 댓글을 연결한다.

- PATCH /comments/:id : 댓글 수정 라우터

- DELETE /comments/:id : 댓글 삭제 라우터

 

- 이제 npm start로 서버를 실행하고 http://localhost:3001로 접속하여 동작을 확인해보자.

Executing (default): SELECT `id`, `name`, `age`, `married`, `comment`, `created_at` FROM `users` AS `users`;
// 이하 생략

- Executing으로 시작하는 SQL 구문을 보고 싶지 않다면 config/config.json의 dialect 속성 밑에 "logging": false를 추가한다.

- 접속 시 GET / 라우터에서 User.findAll 메서드를 호출하므로 그에 따른 SQL문이 실행된다.

- 사용자의 이름을 누르면 사용자가 등록한 댓글이 나온다. 

- 사용자, 댓글 수정 등록.

 

 

- 시퀄라이즈로 모든 데이터베이스 작업을 할 수 없으므로, 나중에는 직접 SQL을 사용해야 하는 경우가 생길 수 있다.

728x90
반응형