본문 바로가기
프로그래밍/웹 개발

[Node.js] Sequelize로 MVC 디자인 패턴 만들기

by 제이콥J 2021. 9. 3.

코드스테이츠의 과제로 Sequelize를 통해 서버와 데이터베이스를 연결해보았다.

Sequelize와 같은 ORM을 사용하면 JavaScript 문법만으로 DB에 접근이 가능하다.

또한 모델을 쉽게 생성할 수 있으며, 마이그레이션으로 DB의 버전 관리가 가능해진다.

Sequelize를 통해 DB와 서버를 연결하고, 모델과 컨트롤러를 만드는 과정은 다음과 같다.

 

1. Sequelize 설치 및 부트스트랩 진행하기

2. Sequelize로 모델을 생성하고, 마이그레이션으로 DB와 연결하기

3. express 문법으로 라우팅을 통해 end point와 연결하기

4. get, post, redirect에 해당하는 컨트롤러 구축하기

5. association을 통한 join 테이블 생성하기

 


Sequelize 설치 및 부트스트랩 진행

 

1. Sequelize를 사용하기 위해 Sequelize 와 sequelize-cli 설치하기

# Sequelize 설치하기
$ npm install --save sequelize

 

# Sequelize 터미널 설치하기
npm install --save-dev sequelize-cli

 

2. 부트스트래핑 (bootstrapping) 진행

- 부트스트래핑 : 프로젝트 초기 단계를 자동으로 설정할 수 있도록 도와주는 일

- 부트스트래핑 후 config, models, migrations, seeders 폴더가 생성됨

 

# 부트스트래핑 명령어 입력
npx sequelize-cli init

 

3. configuration을 통해 DB와 연결하기

- config/config.json 파일에서 데이터베이스명과 MySQL 로그인 정보 입력하기

- config.json 에는 3가지 객체 존재 : development (개발), test (테스트), production (생산)

- model/index.js 를 보면 development가 기본값으로 설정됨

 

// 자동으로 생성된 config/config.json 파일
// 기본값으로 설정되어있는 development 객체에만 값을 입력해줌

{
  "development": {
    "username": "root",
    "password": "비밀번호",
    "database": "database_development",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "database_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "database_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

 

// 자동으로 생성된 models/index.js 파일 내용

'use strict';

const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};

let sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, config);
}

fs
  .readdirSync(__dirname)
  .filter(file => {
    return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
  })
  .forEach(file => {
    const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

module.exports = db;

 

4. 데이터베이스 생성하기

- config.json 파일에 기재된 데이터베이스가 없을 경우, 해당 데이터베이스를 생성하기

- 여기서는 파일 내용대로 'database_development'라는 DB를 생성하기

- Sequelize 명령어를 사용하거나, MySQL에 접속하여 create Database 명령어 사용하기

# config.json 파일의 내용과 일치하는 데이터베이스 만들기
db:create

 

# MySQL에 접속하여 직접 데이터베이스 만들기
CREATE DATABASE database_development

 


모델 생성 후 마이그레이션

 

1. url 모델 생성하기

- id, createdAt, updatedAt 필드 생성 : 자동으로 생성됨

- url, title, visits 필드 생성 :  명령어 입력 또는 파일을 직접 수정하기

- models/url 파일을 수정하여 visits 필드의 default value를 0으로 설정하기

 

# 터미널에 명령어를 입력하기
npx sequelize-cli model:generate --name url --attributes 
url:string,title:string,visits:integer

 

// models/url 파일 내용

'use strict';
const {
  Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
  class url extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
    }
  };
  url.init({
    url : DataTypes.STRING,
    title: DataTypes.STRING,
    visits: {
              type: DataTypes.INTEGER,
              defaultValue: 0
            }
  }, {
    sequelize,
    modelName: 'url',
  });
  return url;
};

 

2. 마이그레이션 진행하기

- models/url (url 모델)의 내용에 맞게 migration 파일도 수정하기

- migration 명령어 입력

 

// migration 파일 내용

'use strict';
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('urls', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      url: {
        type: Sequelize.STRING
      },
      title: {
        type: Sequelize.STRING
      },
      visits: {
        type: Sequelize.INTEGER,
        defaultValue: 0
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('urls');
  }
};

 

# 마이그레이션 진행하기
npx sequelize-cli db:migrate

# 마이그레이션 파일 수정이 필요한 경우, 마이그레이션 해제하기
npx sequelize-cli db:migrate:undo

 


컨트롤러의 메소드에 대해 end point 연결하기

 

1. 메소드와 그에 따른 엔드포인트 : post/links, get/links, get/links:id

 

2. app.js에서 express를 통해 라우터 설정하기

 

// app.js 파일 내용 일부

const indexRouter = require('./routes/index');
const linksRouter = require('./routes/links');

app.use('/', indexRouter);
app.use('/links', linksRouter);

 

3. routes/links.js에서 라우터 설정하기

- 문법 : router.get(경로, 콜백함수)

- app.js에서 경로를 '/links'로 설정해줬기 때문에 여기서는 그냥 '/' 로 설정

// rountes/links.js 파일 내용

const express = require('express');
const router = express.Router();
const linksController = require('../controllers/links');

router.get('/', linksController.get);          // get 메소드 연결
router.post('/', linksController.post);        // post 메소드 연결
router.get('/:id', linksController.redirect);  // redirect 메소드 연결

module.exports = router;

 


post 컨트롤러 구현

1. POST / links 의 API

// content-type: application/json

// payload: 단축시키고 싶은 URL을 url 속성에 담기
{
  "url": "https://www.github.com"
}

// status code: 201 (성공적으로 생성 시)

// response: 방금 생성한 모델의 JSON
{
  "id": 1,
  "url": "https://www.github.com",
  "title": "The world’s leading software development platform · GitHub",
  "visits": 1,
  "createdAt": "2020-07-25T20:07:15.000Z",
  "updatedAt": "2020-07-25T20:07:15.000Z"
}

 

2. 웹사이트 제목을 위해 사용할 modules/utils.js의 파일 내용

onst request = require('request');

const rValidUrl = /^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i;

exports.getUrlTitle = (url, cb) => {
  request(url, function (err, res, html) {
    if (err) {
      console.log('Error reading url heading: ', err);
      return cb(err);
    } else {
      const tag = /<title>(.*)<\/title>/;
      const match = html.match(tag);
      console.log(match.length)
      const title = match ? match[1] : url;
      return cb(err, title);
    }
  });
};

exports.isValidUrl = url => {
  return url.match(rValidUrl);
};

 

3. utils.js 파일의 isValidUrl 함수로 유효성 검사하기

 

4. utils.js 파일의 getUrlTitle 함수를 실행시켜 웹사이트 제목을 가져오기

 

5. getUrlTitle 함수의 인자로 콜백함수를 전달하며 그 안에서 쿼리문 실행

  - 이미 있으면 기존 레코드 리턴하기 : json, 201 code

  - 없으면 새로운 레코드를 만들어 리턴하기 : json, 201 code

 

6.  findOrCreate 키워드 사용

  - where 키 값의 객체 안에 검색 조건 넣어주기

  - 검색 조건에 해당하는 레코드가 없으면, where 조건에 따라 새 레코드 생성

 

코드 작성

const utils = require('../../modules/utils');
const { url } = require('../../models');
// const url = require('../../models').url; 도 가능

post : (req, res) => {

  const reqUrl = req.body.url         // 요청 url 주소

  if (!utils.isValidUrl(reqUrl)) {    // 유효성 검사
    res.status(404).send('Not Found')
  }

  utils.getUrlTitle(reqUrl, (err, title) => {
    
    // 에러 처리
    if (err) {
    res.status(404).send('Not Found')
    }
    
    // findOrCreate 사용
    url.findOrCreate({
      where: {
        url: reqUrl   // 검색 조건
        title: title
      },
    })
    .then(([result, created]) => {   // created는 Boolean 값
      if(!created) {                 // 기존 레코드가 있어서 create 하지 않을 경우
        return res.status(201).json(result);  // result는 조회된 값
      }
      res.status(201).json(result)  // result는 where 조건의 값을 따라 새로 생성된 값
    })
    .catch(err => {
      console.log(err);
      res.sendStatus(500);
    })
  })
}

 


get 컨트롤러 구현

1. findAll 키워드 사용하기

2. json 형식으로 200 코드 리턴하기

 

// async, await 문법 사용

get: async (req, res) => {
  const result = await URLModel.findAll();
  res.status(200).json(result);
}

 


redirect 컨트롤러 구현

1. findOne 키워드를 사용하여 id 값을 통해 레코드를 조회하기

2. 찾는 레코드가 존재하면 update 키워드로 visits column의 값을 1 올리기

3. 해당 url을 redirect 하기

 

코드 작성

- promise, then 문법 사용

  redirect: (req, res) => {
    URLModel
      .findOne({
        where: {                       // 조회하는 레코드 조건
          id: req.params.id
        }
      })
      .then(result => {
        if (result) {
          return result.update({       // update 키워드
            visits: result.visits + 1  // visits 값 올리기
          });
        } else {
          res.sendStatus(204);
        }
      })
      .then(result => {
        res.redirect(result.url);      // 해당 url로 redirect 진행
      })
      .catch(error => {
        console.log(error);
        res.sendStatus(500);
      });
  }
}

 

- async, await 문법 사용

redirect : async (req, res) => {
        
  const id = req.params.id;

  const record = await url.findOne({where:{id:id}})
  
  if(!record) {
    res.sendStatus(404)
  }
  try {
    record.update({visits: record.visits + 1})
    res.redirect(record.url)
  } catch (err) {
    res.sendStatus(500);
  }
}

 


Association을 통한 JOIN 테이블 구현

1. user table (모델) 생성하기

# 터미널에 명령어를 입력하기
npx sequelize-cli model:generate --name user --attributes 
name:string,email:string

 

2. 마이그레이션을 통해 urls 테이블에 userId 필드를 추가하고, foreign key 로 설정하기

 

- 모델 빼고 마이그레이션 파일만 추가하기 

# 터미널 명령어 입력하기
npx sequelize-cli migration:generate --name migration-skeleton

 

- 새 마이그레이션 파일 : urls 테이블에서 userId 필드 추가하고 foreign key 설정하기

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    // field 추가
    await queryInterface.addColumn('urls', 'userId', Sequelize.INTEGER);

    // foreign key 연결
    await queryInterface.addConstraint('urls', {
      fields: ['userId'],
      type: 'foreign key',
      name: 'FK_any_name_you_want',
      references: {
        table: 'users',
        field: 'id'
      },
      onDelete: 'cascade',
      onUpdate: 'cascade'
    });
  },

  down: async (queryInterface, Sequelize) => {
    await queryInterface.removeConstraint('urls', 'FK_any_name_you_want');
    await queryInterface.removeColumn('urls', 'userId');
  }
};

 

3. models/index.js 에 association 관계 명시하기 (belongsTo, hasMany)

// models/index.js 파일

db.sequelize = sequelize;
db.Sequelize = Sequelize;

// 아래 부분을 추가하기 : associations 설정
const { url, user } = sequelize.models;
url.belongsTo(user);
user.hasMany(url);

module.exports = db;

 

반응형

댓글