📍 이전 포스팅 캡스톤일지 ~ 10.05 NodeJS TDD 웹개발 훑어보기(1)
TDD로 하는 API 개발
- READ
📄 index.js1 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) }); }); });
- DELETE
📄 index.js1 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) }); }); });
- CREATE
📄 index.js1 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) }); }); });
- UPDATE
📄 index.js1 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() 해당 부분만 테스트 실행
라우터 클래스와 컨트롤러 분리
- 🗂 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 지원 }
- 🗂 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
- 📄 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
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 초기화하지 않고 내용 유지
🗂 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"
},
ORM 객체를 이용한 API 개발
🗂 api > 🗂 user > 📄 index.ctrl.js 기존의 사용자 CRUD 코드 수정
1
const models = require('../../models');
- 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); });
- DELETE
1 2 3 4 5
models.User.destroy({ where: {id} }).then(() => { res.status(204).end(); })
- 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
- 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에 작성한 쿼리 실행하도록 할것 같긴 한디.. 그래도 알아두면 좋으니까 또 혹시몰라 쓰게될지도
노드 재밌구먼 자바는 언제 다시 시작하지..