상세 컨텐츠

본문 제목

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

학부/캡스톤(a.k.a 졸작)

by ranlan 2021. 10. 23. 11:08

본문

728x90

이전 포스팅 -> 2021.10.10 - [capstone] - 캡스톤 일지 ~10.05

 

캡스톤 일지 ~10.05

전날에는 tensorflow.js를 한번 훑어보고 다음 날 노드 공부를 시작하였다. 이전에 노드 공부하며 들었던 인프런 강의 중 괜찮았던 강의가 몇 개 생각나 다시 들을 예정 나 이렇게나 인프런 잘 이용

juran-devblog.tistory.com

이전에 듣던 TDD 노드 강의 드디어 완강!

 


TDD로 하는 API 개발

1) READ

📄 index.js

// 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

// 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

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

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

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

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

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

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

 

Express 4.x - API 참조

Express 4.x API express() Creates an Express application. The express() function is a top-level function exported by the express module. var express = require('express') var app = express() Methods express.json([options]) This middleware is available in Ex

expressjs.com

express4.x 이전

const bodyParser = require('body-parser');

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

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

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

 

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

 

 

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

1) 🗂 api > 🗂 user > 📄 index.ctrl.js 

라우팅 작업에 필요한 메서드를 작성한 컨트롤러

// 컨트롤러
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에 대한 라우팅 설정

// '/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에서 호출

const user = require('./api/user'); // 해당 폴더에서 index.js 알아서 가져옴
app.use('/users', user); // /users 관련 라우팅 모두 담당

 

 

테스트 환경 개선

📑 package.json

{
  "scripts": {
      "test": "NODE_ENV=test mocha api/user/user.spec.js",
      "start": "node index.js"
    },
    ...
}

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

 

📑 index.js

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

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

 

 

데이터베이스 ORM

이전 JPA 포스팅

npm i sqlite3 --save
npm i sequelize --save

사용할 데이터베이스는 파일 형태의 데이터베이스  sqlite3  (이전에 파이썬할때도 써봤었음) https://www.npmjs.com/package/sqlite3

노드에서 ORM 사용하기  sequlize  https://www.npmjs.com/package/sequelize

 

sequelize

Multi dialect ORM for Node.JS

www.npmjs.com

 

📄 models.js

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

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 초기화하지 않고 내용 유지

 

 

🗂 bin > 📄 app.js

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

"scripts": {
    "test": "...",
    "start": "node bin/www.js"
  },

 

 

ORM 객체를 이용한 API 서버 개발

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

const models = require('../../models');

1) READ

// 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

models.User.destroy({
    where: {id}
}).then(() => {
    res.status(204).end();
})

3) CREATE

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 발생 -> 409 반환

 

4) UPDATE

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에 작성한 쿼리 실행하도록 할것 같긴 한디..

그래도 알아두면 좋으니까 또 혹시몰라 쓰게될지도

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

 

 

728x90

관련글 더보기

댓글 영역