Home 캡스톤일지 | ~ 10.21 NodeJS TDD 웹개발 훑어보기(2)
Post
Cancel

캡스톤일지 | ~ 10.21 NodeJS TDD 웹개발 훑어보기(2)

📍 이전 포스팅 캡스톤일지 ~ 10.05 NodeJS TDD 웹개발 훑어보기(1)


TDD로 하는 API 개발

  1. READ
    📄 index.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
     // limit만큼 사용자 조회
     app.get('/users', (req, res) => {
         req.query.limit = req.query.limit || 10;
         const limit = parseInt(req.query.limit, 10);
    
         if (Number.isNaN(limit)) {
             return res.status(400).end(); 예외1) 숫자가 아닐 경우 400 반환
         }
         res.json(users.slice(0, limit)); //res.body
     });
    
     // 아이디로 사용자 조회
     app.get('/users/:id', function(req, res) {
         const id = parseInt(req.params.id, 10);
         if (Number.isNaN(id)) return res.status(400).end(); // 예외1)숫자가 아닐 경우 400 반환
    
         const user = users.filter((user) => { // 특정 조건에 맞는 객체 array 반환
             return user.id === id
         })[0];
         // const user = users.filter((user) => user.id === id)[0];
    
         if (!user) return res.status(404).end(); // 예외2) 유저가 없을 경우 404 반환
         res.json(user);
     });
    

    📄 index.spec.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    
     // limit만큼 사용자 조회
     describe('GET /users는', () => {
         describe('성공 시', () => {
             it('유저 객체를 담은 배열을 응답한다.', (done) => { // 비동기 처리
                 request(app)
                     .get('/users')
                     .end((err, res) => {
                         console.log(res.body);
                         res.body.should.be.instanceOf(Array);
                         done();
                     });
             });
             it('최대 limit 갯수만큼 응답한다', (done) => {
                 request(app)
                     .get('/users?limit=2')
                     .end((err, res) => {
                         console.log(res.body);
                         res.body.should.have.lengthOf(2)
                         done();
                     });
             });
         });
    
         describe('실패 시', () => {
             it('예외1) limit이 숫자형이 아니면 400을 응답한다.', (done) => {
                 request(app)
                     .get('/users?limit=two')
                     .expect(400)
                     .end(done);
             });
         });
     });
    
     // 사용자 아이디 조회
     describe('GET /users/:id는', () => {
         describe('성공 시', () => {
             it('id가 1인 유저 객체를 반환한다.', (done) => {
                 request(app)
                 .get('/users/1')
                 .end((err, res) => {
                     console.log(res.body)
                     res.body.should.have.property('id', 1);
                     done();
                 });
             });
         });
            
         describe('실패 시', () => {
             it('예외1) id가 숫자가 아닐 경우 400으로 응답한다', (done) => {
                 request(app)
                     .get('/users/one')
                     .expect(400)
                     .end(done);
             });
             it('예외2) id로 유저를 찾을 수 없는 경우 404로 응답한다', (done) => {
                 request(app)
                 .get('/users/999')
                 .expect(404)
                 .end(done)
             });
         });
     });
    
  2. DELETE
    📄 index.js
    1
    2
    3
    4
    5
    6
    7
    
     app.delete('/users/:id', (req, res) => {
         const id = parseInt(req.params.id, 10);
         if (Number.isNaN(id)) return res.status(400).end(); // 예외1) 숫자가 아닐 경우 400 반환
    
         users = users.filter(user => user.id !== id); // 해당 아이디가 아닌 유저만 반환
         res.status(204).end();
     });
    

    📄 index.spec.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
     describe('DELETE /users/:id', () => {
         describe('성공 시', () => {
             it('1) 204를 응답한다', (done) => {
                 request(app)
                     .delete('/users/1')
                     .expect(204)
                     .end(done)
             });
         });
         describe('실패 시', () => {
             it('예외1) id가 숫자가 아닐 경우 400으로 응답한다.', (done) => {
                 request(app)
                     .delete('/users/one')
                     .expect(400)
                     .end(done)
             });
         });
     });
    
  3. CREATE
    📄 index.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
     app.post('/users', (req, res) => {
         const name = req.body.name;
         if(!name) return res.status(400).end(); // 예외1) 이름 누락시 400 반환
    
         const isConflic = users.filter(user => user.name === name).length 
         if (isConflic) return res.status(409).end(); // 예외2) 이름 중복 시 409 반환
    
         const id = Date.now();
         const user = {id, name};
         users.push(user);
         res.status(201).json(user);
     });
    

    📄 index.spec.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
     describe('POST /users', () => {
         describe('성공 시', () => {
             let name = 'daniel',
                 body;
             before(done => { // 중복되는 코드 묶어서 처음 한번에 실행
                 request(app)
                     .post('/users')
                     .send({name})
                     .expect(201)
                     .end((err, res) => {
                         body = res.body;
                         done();
                     });
             });
             it('생성된 유저 객체를 반환한다.', () => {
                 body.should.have.property('id');
             });
             it('입력한 name을 반환한다.', () => {
                 body.should.have.property('name', name);
             });
         });
    
         describe('실패 시', () => {
             it('예외1) name 파리미터 누락 시 400을 반환한다.', (done) => {
                 request(app)
                     .post('/users')
                     .send({})
                     .expect(400)
                     .end(done)
             });
             it('예외2) name이 중복일 경우 409를 반환한다.', (done) => {
                 request(app)
                     .post('/users')
                     .send({name: 'daniel'})
                     .expect(409)
                     .end(done)
             });
         });
     });
    
  4. UPDATE
    📄 index.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
     app.put('/users/:id', (req, res) => {
         const id = parseInt(req.params.id, 10);
         if (Number.isNaN(id)) return res.status(400).end(); // 예외1) 숫자가 아닐 경우 400 반환
    
         const name = req.body.name;
         if(!name) return res.status(400).end(); // 예외2) 이름 누락시 400 반환
         const isConflic = users.filter(user => user.name === name).length  // 예외4) 이름 중복시 409 반환
         if (isConflic) return res.status(409).end();
    
         const user = users.filter(user => user.id === id)[0];
         if (!user) return res.status(404).end(); // 예외3) 없는 유저일때 404 반환
            
         user.name = name;
            
         res.json(user);
     });
    

    📄 index.spec.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
     describe('PUT /users/:id', () => {
         describe('성공 시', () => {
             it('변경된 name을 응답한다.', (done) => {
                 const name = 'chally';
                 request(app)
                     .put('/users/3')
                     .send({name})
                     .end((err, res) => {
                         res.body.should.have.property('name', name);
                         done();
                     });
             });
         });
         describe('실패 시', () => {
             it('예외1) 정수가 아닌 id인 경우 400을 반환한다.', (done) => {
                 request(app)
                     .put('/users/one')
                     .expect(400)
                     .end(done)
             });
             it('예외2) name이 누락될 경우 400를 반환한다.', (done) => {
                 request(app)
                 .put('/users/one')
                 .send({})
                 .expect(400)
                 .end(done)
             });
             it('예외3) 없는 유저일 경우 404를 응답한다.', (done) => {
                 request(app)
                 .put('/users/999')
                 .send({name: 'foo'})
                 .expect(404)
                 .end(done)
             });
             it('예외4) name이 중복될 경우 400를 반환한다.', (done) => {
                 request(app)
                 .put('/users/3')
                 .send({name: 'bek'})
                 .expect(409)
                 .end(done)
             });
         });
     });
    

req.body express 모듈에서는 body 지원하지 않음으로 body-parser, multer(이미지 데이터 처리)등의 미들웨어 필요
http://expressjs.com/ko/api.html#req.body

express4.x 이전

1
2
3
4
const bodyParser = require('body-parser');

app.user(bodyParser.json());
app.user(bodyParser.urlencoded({ extended: true}));

express4.x 이후부터는 body parser 기본 제공

1
2
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

테스트코드 only() 해당 부분만 테스트 실행

라우터 클래스와 컨트롤러 분리

image

  1. 🗂 api > 🗂 user > 📄 index.ctrl.js
    라우팅 작업에 필요한 메서드를 작성한 컨트롤러
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    
     // 컨트롤러
     var users = [
         {id: 1, name: 'alice'},
         {id: 2, name: 'bek'},
         {id: 3, name: 'chris'},
     ]
    
     const index = function (req, res) {
         req.query.limit = req.query.limit || 10;
         const limit = parseInt(req.query.limit, 10);
         if (Number.isNaN(limit)) {
             return res.status(400).end();
         }
         res.json(users.slice(0, limit));
     };
    
     const show = function(req, res) {
         const id = parseInt(req.params.id, 10);
         if (Number.isNaN(id)) return res.status(400).end(); // 예외1) 숫자가 아닐 경우 400 반환
    
         const user = users.filter((user) => user.id === id)[0];
         if (!user) return res.status(404).end(); // 예외2) 유저가 없을 경우 404 반환
    
         res.json(user);
     }
    
     const destroy =  (req, res) => {
         const id = parseInt(req.params.id, 10);
         if (Number.isNaN(id)) return res.status(400).end();
            
         users = users.filter(user => user.id !== id);
    
         res.status(204).end();
     }
    
     const create = (req, res) => {
         const name = req.body.name;
         if(!name) return res.status(400).end(); // 예외1) 이름 누락
    
         const isConflic = users.filter(user => user.name === name).length  // 예외2) 이름 중복
         if (isConflic) return res.status(409).end();
    
         const id = Date.now(); // db에서 아이디 생성
         const user = {id, name};
         users.push(user);
         res.status(201).json(user);
     }
    
     const update = (req, res) => {
         const id = parseInt(req.params.id, 10);
         if (Number.isNaN(id)) return res.status(400).end(); // 예외1) 정수가 아닌 아이디
    
         const name = req.body.name;
         if(!name) return res.status(400).end(); // 예외2) 이름 누락
    
         const isConflic = users.filter(user => user.name === name).length  // 예외4) 이름 중복
         if (isConflic) return res.status(409).end();
            
         const user = users.filter(user => user.id === id)[0];
         if (!user) return res.status(404).end(); // 예외3) 없는 유저
    
         user.name = name;
         res.json(user);
     }
    
     module.exports = {
         // index: index,
         // show: show, 
         // destroy: destroy,
         // create: create, 
         // update: update
         index, show, destroy, create, update // ES6 지원
     }
    
  2. 🗂 api > 🗂 user > 📄 index.js
    user 관련 작업을 모아둔 user 폴더의 메인 파일로 사용하는 url에 대한 라우팅 설정
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
     // '/users/...' api 라우팅 설정
     const express = require('express');
     const router = express.Router();
     const ctrl = require('./user.ctrl');
    
     router.get('/', ctrl.index);
    
     router.get('/:id', ctrl.show);
    
     router.delete('/:id', ctrl.destroy);
    
     router.post('/', ctrl.create);
    
     router.put('/:id', ctrl.update);
    
     module.exports = router; // router 객체 export
    
  3. 📄 index.js
    생성한 user 라우터 메인 index.js에서 호출
    1
    2
    
     const user = require('./api/user'); // 해당 폴더에서 index.js 알아서 가져옴
     app.use('/users', user); // /users 관련 라우팅 모두 담당
    

테스트 환경 개선

📑 package.json

1
2
3
4
5
6
7
{
  "scripts": {
      "test": "NODE_ENV=test mocha api/user/user.spec.js",
      "start": "node index.js"
    },
    ...
}

테스트 실행 시 노드 환경변수 생성하여 테스트 환경임을 알려줌
📑 index.js

1
2
3
if (process.env.NODE_ENV !== 'test') {
  app.use(morgan('dev'));
}

테스트 환경일때만 morgan 로그 남김

데이터베이스 ORM

image

1
2
npm i sqlite3 --save
npm i sequelize --save

sqlite3 사용할 데이터베이스는 파일 형태의 데이터베이스 https://www.npmjs.com/package/sqlite3
sequlize 노드에서 ORM 사용하기 https://www.npmjs.com/package/sequelize

📄 models.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Sequelize = require('sequelize');
const sequelize = new Sequelize({
    dialect: 'sqlite', // 파일 형식의 데이터베이스 sqlite
    storage: './db.sqlite',
    // logging: false
});

const User = sequelize.define('User', {
    name: {
        type: Sequelize.STRING,
        unique: true // 제약조건
    }, 
    
});

module.exports = {Sequelize, sequelize, User};

사용할 데이터베이스 연동과 객체 생성
🗂 bin > 📄 sync-db.js

1
2
3
4
5
6
7
8
const models = require('../models');

module.exports = () => {
    const options = {
        force: process.env.NODE_ENV === 'test' ? true : false
    };
    return models.sequelize.sync({options}); 
}

데이터베이스 동기화

  • force: true
    테스트 환경에서(package.json 스크립트에서 테스트 시 노드 환경변수 test로 설정) 동기화할때마다 DB 초기화
  • force: false
    동기화할때마다 DB 초기화하지 않고 내용 유지

image

🗂 bin > 📄 app.js

1
2
3
4
5
6
7
8
9
const app = require('../index');
const syncDB = require('./sync-db');
const port = 3000;

syncDB().then(_ =>{ // 비동기
  app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
  });
})

index.js에서 앱 구동부분을 빼고 해당 스크립트에서 데이터베이스 동기화와 앱 구동 한번에 실행 📄 package.json scripts

1
2
3
4
"scripts": {
    "test": "...",
    "start": "node bin/www.js"
  },

image

ORM 객체를 이용한 API 개발

🗂 api > 🗂 user > 📄 index.ctrl.js 기존의 사용자 CRUD 코드 수정

1
const models = require('../../models');
  1. READ
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
     // limit만큼 조회
     models.User.findAll({
             limit: limit
         })
         .then(users => {
             res.json(users);
         });
            
     // 아이디로 사용자 조회
     models.User.findOne({
         where: {
             id: id
         }
     }).then(user => {
         if (!user) return res.status(404).end();
         res.json(user);
     });
    
  2. DELETE
    1
    2
    3
    4
    5
    
     models.User.destroy({
         where: {id}
     }).then(() => {
         res.status(204).end();
     })
    
  3. CREATE
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     models.User.create({name}) 
         .then(user => { // 해당 이름을 가진 유저 생성
             res.status(201).json(user);
         })
         .catch(err => {
             if (err.name === 'SequelizeUniqueConstraintError') {
                 return res.status(409).end();
             }
             res.status(500).end();
         });
    

    models.js에서 작성한 unique: true 조건에 의해 입력값이 중복인 경우 SequelizeUniqueConstraintError

  4. UPDATE
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
     models.User.findOne({where : {id}}) // 해당 이이디의 유저 찾음
         .then(user => {
             if(!user) return res.status(404).end();
    
             user.name = name; // 이름 변경
             user.save()
                 .then(_=> {
                     res.json(user);
                 })
                 .catch(err => {
                     if (err.name === 'SequelizeUniqueConstraintError') {
                         return res.status(409).end();
                     }
                     res.status(500).end();
                 });
         });
    

테스트 코드 모두 통과하면 끝!



노드 강의 드디어 완강했다! ORM 시퀄라이저 안쓰고 그냥 DB connection에 작성한 쿼리 실행하도록 할것 같긴 한디.. 그래도 알아두면 좋으니까 또 혹시몰라 쓰게될지도

노드 재밌구먼 자바는 언제 다시 시작하지..

This post is licensed under CC BY 4.0 by the author.

캡스톤일지 | ~ 10.16 robotjs 이용한 마우스 커서 이동

캡스톤일지 | ~ 10.26 미디어파이프 오류 해결과 socket.io 예제