Node.js를 이용한 모바일 API 서버 개발 사례
-
V8
-
Non-blocking Event I/O
-
CommonJS
-
ES6 support (node.green)
Node.js
// sum.js
const sum = (a, b) => a + b;
module.exports = sum;
// app.js
const sum = require('./sum');
sum(1, 2); // 3
-
명령어
-
node
-
node
-
node app.js
-
-
npm
- npm init
- npm install [package name]
- npm install [package name] --save
- npm install [package name] --save-dev
- npm install
-
npm start
-
npm test
-
npm run [script name]
-
Node.js
// package.json
{
"name": "apiserver",
"scripts": {
"start": "node bin/www",
"test": "NODE_ENV=test node_modules/.bin/mocha app/**/*.spec.js"
},
"dependencies": {
"express": "^4.14.0"
},
"devDependencies": {
"mocha": "^3.1.2"
}
}
Hello world (node)
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Run at http://${hostname}:${port}/`);
});
-
Web framework for nodejs
-
Application
-
Middleware
-
Request
-
Response
-
Router
Hello world (express)
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!\n')
});
app.listen(3000, () => {
console.log(`Run at http://localhost:3000`)
});
Express vs http
- 요청(Reqeust) 처리
- 쿼리문자열 파싱
- 리퀘스트 바디 파싱 (body-parser)
- 라우팅 처리
- 구조적인 코드를 유지할 수 있음
- 응답(Response) 처리
- 응답 헤더 자동 설정
- 미들웨어로 추가할 수 있는 기능이 다양함
- 로깅(morgan), 인증(passport), 파라매터 검증, 세션 ...
Application
- 익스프레스 인스턴스
- 미들웨어 세팅
- 라우팅 세팅
- 요청대기상태(listen)
const express = require('express');
const app = express();
app.use(require('morgan')('dev'));
app.get('/', (req, res) => res.send('Hello World!\n'));
app.listen(3000, () => console.log('Run at http://localhost:3000'));
Request
- http 모듈의 request 객체를 사용함
- express에서 아래 객체를 자동으로 추가해줌
- req.params
- req.query
- req.body
app.get('/:id', (req, res) => {
const id = req.params.id; // /1
const limit = req.query.limit; // ?limit=10
const body = req.body; // {name: 'chris'}
});
Response
- http 모듈의 response 객체를 사용함
- express에서 아래 메소드를 자동으로 추가해줌
- res.send()
- res.status()
- res.json()
- 각 메소드별로 헤더 자동 설정됨
app.get('/:id', (req, res) => {
res.status(204).end();
res.send('hello world');
res.json([{msg: 'hello world'}]);
});
Router
- 라우팅 설정을 위한 Router 클래스
- API 서버의 핵심인 라우팅 로직을 구조적으로 구현할수 있음
const user = require('./user');
app.use('/users', user)
// user.js
const router = require('express').Router()
router.get('/', (req, res) => res.send('user list'));
router.post('/', (req, res) => res.send('created user'));
module.exports = router;
Middleware
- req → middlewares... → route → middlewares... → res
const app = require('express')();
app.use((req, res, next) => {
console.log('middleware 1');
next();
});
app.use((req, res, next) => {
console.log('middleware 2');
next();
});
app.get('/', (req, res, next) => {
console.log('middleware 3');
next();
}, (req, res) => {
console.log('router GET /');
res.send('hello world\n')
});
app.listen(3000, _=> console.log('Run server'));
middleware 1
middleware 2
middleware 3
router Get /
Middleware
- error middleware 1
const app = require('express')();
app.use((req, res, next) => {
console.log('middleware 1');
next('error 1');
});
app.use((error, req, res, next) => {
console.log('middleware 2', error);
next();
});
app.get('/', (req, res, next) => {
console.log('middleware 3');
next();
}, (req, res) => {
console.log('router GET /');
res.send('hello world\n')
});
app.listen(3000, _=> console.log('Run server'));
middleware 1
middleware 2, error1
middleware 3
router Get /
Middleware
- error middleware 2
const app = require('express')();
app.use((req, res, next) => {
console.log('middleware 1');
next('error 1');
});
app.use((error, req, res, next) => {
console.log('middleware 2', error);
next(error);
});
app.get('/', (req, res, next) => {
console.log('middleware 3');
next();
}, (req, res) => {
console.log('router GET /');
res.send('hello world\n')
});
app.listen(3000, _=> console.log('Run server'));
middleware 1
middleware 2, error1
error 1
Middleware
- Express middlewares
- Custom middleware
- normal middleware
-
app.use((req,res,next)=> next())
-
- error middlware
-
app.use((err, req,res,next)=> next())
-
- normal middleware
API 개발
-
GET /usres/:id
-
POST /usres
-
PUT /usres/:id
-
DELETE /usres/:id
시작
const express = require('express');
const logger = require('morgan');
// 익스프레스 객체 생성
const app = express()
// 유저 데이터
let users = [
{id: 1, name: 'alice'},
{id: 2, name: 'bek'},
{id: 3, name: 'chris'}
];
// 로거 설정
app.use(logger('dev'));
// 요청 대기 상태 진입
app.listen(3000, () => console.log('Listening on port 3000'));
GET /users/:id
// GET /users API 구현
app.get('/users', (req, res) => res.json(users));
// GET /users/:id API 구현
app.get('/users/:id', (req, res) => {
const user = users.filter(u => u.id == req.params.id)[0];
if (user) return res.json(user);
// 사용자가 없는 경우 404 상태코드 응답
res.status(404).end();
});
POST /users
// 바디 파서 미들웨어 추가
const bodyParser = require('body-parser');
// 바디 파서 미들웨어 설정
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
// post /users API 구현
app.post('/users', (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = {id: Date.now(), name: req.body.name}
users.push(user);
res.status(201).json(user);
});
PUT /users/:id
app.put('/users/:id', (req, res) => {
// 파라매터 에러 처리 (400 반환)
if (!req.body.name) return res.status(400).end();
const user = users.filter(u => u.id == req.params.id)[0];
// 없는 유저일 경우 에러처리 (404 반환)
if (!user) return res.status(404).end();
user.name = req.body.name;
res.json(user);
});
DELETE /users/:id
app.delete('/users/:id', (req, res) => {
users = users.filter(u => u.id != req.params.id);
res.status(204).end();
});
Express Router 적용
app.use('/users', require('./api/user'));
// api/user/index.js
// 라우터 객체 생성
const router = require('express').Router();
let users = [
{id: 1, name: 'alice'},
{id: 2, name: 'bek'},
{id: 3, name: 'chris'}
];
router.get('', (req, res) => res.json(users));
router.get('/:id', (req, res) => {
const user = users.filter(u => u.id == req.params.id)[0];
if (user) return res.json(user);
res.status(404).end();
});
router.post('', (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = {id: Date.now(), name: req.body.name}
users.push(user);
res.status(201).json(user);
});
router.put('/:id', (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = users.filter(u => u.id == req.params.id)[0];
if (!user) return res.status(404).end();
user.name = req.body.name;
res.json(user);
});
router.delete('/:id', (req, res) => {
users = users.filter(u => u.id != req.params.id);
res.status(204).end();
});
// 라우터를 외부로 노출
module.exports = router;
Express Router 적용
// api/user/index.js
// 컨트롤러로 비지니스 로직 분리
const ctrl = require('./user.ctrl');
router.get('', ctrl.index);
router.get('/:id', ctrl.show);
router.post('', ctrl.create);
router.put('/:id', ctrl.update);
router.delete('/:id', ctrl.destory);
// api/user/user.ctrl.js
let users = [
{id: 1, name: 'alice'},
{id: 2, name: 'bek'},
{id: 3, name: 'chris'}
];
const index = (req, res) => res.json(users);
const show = (req, res) => {
const user = users.filter(u => u.id == req.params.id)[0];
if (user) return res.json(user);
res.status(404).end();
};
const create = (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = {id: Date.now(), name: req.body.name}
users.push(user);
res.status(201).json(user);
};
const update = (req, res) => {
if (!req.body.name) return res.status(400).end();
const user = users.filter(u => u.id == req.params.id)[0];
if (!user) return res.status(404).end();
user.name = req.body.name;
res.json(user);
};
const destory = (req, res) => {
users = users.filter(u => u.id != req.params.id);
res.status(204).end();
};
module.exports = {index, show, create, update, destory};
API 개발 2
-
데이터베이스 연동
ORM
-
Sequelize: nodejs에서 사용하는 sql 디비용 ORM
-
sqlite: 개발, 테스용 / mysql: 운영용
// models/index.js
const Sequelize = require('sequelize');
// 데이터베이스 연결
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'db.development.sqlite'
});
// 유저 모델링
const User = sequelize.define('user', {
name: Sequelize.STRING,
});
module.exports = {sequelize, User};
// index.js
// 디비 초기화
models.sequelize.sync({force: true}).then(() => {
console.log('database is synced');
app.listen(3000, () => console.log('Listening on port 3000'));
});
ORM
const models = require('../../models');
const index = (req, res) => {
models.User.findAll().then(users => res.json(users));
}
const show = (req, res) => {
models.User.findOne({where: {id: req.params.id}}).then(user => {
if (!user) return res.status(404).end();
res.json(user);
});
};
const create = (req, res) => {
if (!req.body.name) return res.status(400).end();
models.User.create({name: req.body.name}).then(user => {
res.status(201).json(user);
});
};
const update = (req, res) => {
if (!req.body.name) return res.status(400).end();
models.User.findOne({where: {id: req.params.id}}).then(user => {
if (!user) return res.status(404).end();
user.name = req.body.name;
user.save().then(_ => res.json(user));
});
};
const destory = (req, res) => {
models.User.destroy({where: {id: req.params.id}}).then(() => {
res.status(204).end();
});
};
module.exports = {index, show, create, update, destory};
-
findAll(), findOne(), create(), update(), destroy()
테스트
-
Postman
-
Mocha, should, supertest
-
환경의 분리
Postman, Curl
- 수작업, 느린 속도, 한계
- 테스트 러너
- 웹스톰과 연동하면 디버깅도 편함
describe('test suite', () => {
before('setup mokup', () => ...);
after('tear down mokup', () => ...);
it('test case 1', () => ...);
it('test case 2', () => ...);
});
- 노드 기본 모듈인 assert보다는 서드파티 검증 모듈을 사용하도록 권장
describe('test suite', () => {
it('test case 1', () => {
users.should.be.an.instanceOf(Array)
users.should.have.length(3);
users[0].should.have.properties('id', 'name', 'caretedAt')
users[0].should.have.property('name', 'Alice')
});
});
- REST API 테스트 라이브러리
const app = require('./app');
const request = require('supertest');
describe('...', () => {
it('...', done => {
request(app)
.get('/users')
.expect(200)
.done((err, res) => {
if (err) throw err;
res.body.should.be.instanceOf(Array);
done()
});
});
})
서버 코드와 실행 코드를 분리
- app: 서버 어플리케이션 코드
- bin: app를 실제 구동하는 코드
// bin/www.js
const models = require('../app/models');
const app = require('../app');
models.sequelize.sync({force: true}).then(() => {
console.log('database is synced');
app.listen(3000, () => console.log('Listening on port 3000'));
});
// app/index.js
const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const app = express();
if (process.env.NODE_ENV === 'development') app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use('/users', require('./api/user'));
// for test
module.exports = app;
데이터베이스 분리
- 노드 환경 변수는 "development", "production", "test" 세가지 값을 사용
- 각 환경에 따라 디비 연결을 분리
// config/index.js
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const config = {
development: {
db: {
dialect: 'sqlite',
storage: 'db.development.sqlte'
}
},
test: {
db: {
dialect: 'sqlite',
storage: ':memory:',
logging: false
}
},
production: {
db: {
username: 'root',
password: 'root',
database: 'apiserver_production',
host: '127.0.0.1',
dialect: 'mysql',
logging: false
}
}
}
module.exports = config[process.env.NODE_ENV];
// models/index.js
const sequelize = new Sequelize(config.db);
테스트 코드 작성
const should = require('should');
const request = require('supertest');
const app = require('../../');
const models = require('../../models');
describe('User', () => {
const users = [
{name: 'alice'},
{name: 'bek'},
{name: 'chris'}
]
before('sync database', () => models.sequelize.sync({force: true}));
before('insert seed data', () => models.User.bulkCreate(users));
describe('GET /users', () => {
it('should return user array', done => {
request(app)
.get('/users')
.expect(200)
.end((err, res) => {
if (err) throw err;
res.body.should.be.instanceOf(Array).with.length(users.length)
done();
});
});
});
describe('GET /users/:id', () => {
it('should return user object', done => {
request(app)
.get('/users/1')
.expect(200)
.end((err, res) => {
if (err) throw err;
res.body.should.have.property('name', users[0].name)
done();
});
});
});
describe('POST /users', () => {
it('should return new user', done => {
request(app)
.post('/users')
.send({name: 'daniel'})
.expect(201)
.end((err, res) => {
if (err) throw err;
res.body.should.have.property('name', 'daniel')
done();
});
});
});
describe('PUT /users/:id', () => {
it('should update username', done => {
request(app)
.put('/users/4')
.send({name: 'david'})
.expect(200)
.end((err, res) => {
if (err) throw err;
res.body.should.have.property('name', 'david')
done();
});
});
});
describe('DELETE /users/:id', () => {
it('should delete user by id', done => {
request(app)
.delete('/users/4')
.expect(204)
.end(done);
});
});
});
실행 스크립트 등록
- package.json에 실행 스크립트를 추가할 수 있음
- npm start
- npm test
"scripts": {
"start": "node bin/www",
"test": "NODE_ENV=test node_modules/.bin/mocha app/api/user/user.spec.js"
},
문서화
-
Github wiki + postman
-
apidoc + postman
-
swagger
깃헙 위키
- 문서 일관성 유지 어려움
- FE개발자도 포스트맨으로 테스트하며 개발
-
POST /users
-
input
- name
- gender
- mobile
- nationality
- dob
- ...
-
response
- userId
- name
- ...
-
input
- 코드에 주석으로 API 문서를 작성하는 형식
- 코드보다 주석이 많은 경우가 빈번함
Swagger
- 스웨거 문법에 맞춰 문서를 작성함 (json, yaml 형식)
- swagger-ui를 api 서버에 장착하여 개발
- 문서와 api 테스트 툴을 함께 사용할 수 있는 장점
Swagger spec
스웨거 스펙을 이용해 문서 작성
const paths = {
'/users': {
get: {
tags: ['User'],
summary: 'Get users',
operationId: 'userUsers',
produces: ['application/json'],
parameters: [
],
responses: {
200: {
description: 'Success',
schema: {
type: 'array',
items: {'$ref': '#/definitions/User'}
}
}
}
},
post: {
tags: ['User'],
summary: 'Create users',
operationId: 'postUsers',
produces: ['application/json'],
parameters: [{$ref: '#/parameters/User'}],
responses: {
201: {
description: 'Created',
schema: {"$ref": "#/definitions/User"}
},
400: {description: 'BadRequest'}
}
}
}
}
const parameters = {
User: {
name: 'user',
in: 'body',
required: true,
schema: {
type: 'object',
properties: {
name: {type: 'string', default: 'chris'}
}
}
}
}
const definitions = {
User: {
type: 'object',
properties: {
id: {type: 'number'},
name: {type: 'string'},
createdAt: {type: 'string', format: 'dateTime'},
updatedAt: {type: 'string', format: 'dateTime'}
}
}
};
module.exports = {
"swagger": "2.0",
"info": {
"title": "apiserver",
"description": "apiserver API Documents",
"termsOfService": "",
"contact": {
"name": "Chris, WePlanet"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0.0"
},
"host": "",
"basePath": "/",
"schemes": [
"http"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
paths: paths,
parameters: parameters,
definitions: definitions
};
Swagger spec
작성한 문서 호스팅
// config/swagger/index.js
const express = require('express');
const path = require('path')
const setupDocument = app => {
app.get('/swagger/doc', (req, res) => {
const doc = require('./v1.doc');
res.json(doc);
});
};
module.exports = app => {
setupDocument(app);
}
Swagger ui
문서를 렌더링할 swagger-ui 툴을 프로젝트에 추가
// config/swagger/index.js
const setupSwaggerUi = app => {
app.use('/swagger', (req, res, next) => {
if (req.url === '/') res.redirect('/swagger?url=doc');
else next();
}, express.static(path.join(__dirname, '../../../node_modules/swagger-ui/dist')));
}
module.exports = app => {
setupDocument(app);
setupSwaggerUi(app);
}
// index.js
// 스웨거 설정
const swagger = require('./config/swagger');
swagger(app);
Swagger ui
한계
- 정적파일(Static files)?
- → 정정파일은 Nginx로 처리하는 것이 성능상 유리
- Uncatched Exception으로 서버 죽는 문제
- Cpu Bound Job?
- → AWS Lambda로 기능 분리 (Resize images, Cron)
배포
- 우분투에 노드 설치 (참고)
- Git hooks을 이용한 배포 자동화
- 배포서버에 git 저장소 설치
- git init --bare --shared
- post-receive 후커 작성
- 배포서버 리모트 추가
- git remote add dev-server [배포서버 깃 주소]
- 배포서버로 소스코드 배포
- git push dev-server master
- 배포서버에 git 저장소 설치
# hooks/post-receive
#!/bin/bash
APP_NAME=apiserver
APP_DIR=$HOME/$APP_NAME
REVISION=$(expr substr $(git rev-parse --verify HEAD) 1 7)
GIT_WORK_TREE=$APP_DIR git checkout -f
cd $APP_DIR
forever stop apiserver
forever start --uid apiserver npm bin/www
배포
- AWS Ealstic Beanstalk을 이용한 배포 자동화
- EC2, 로드밸런서 등을 자동으로 구성해주는 서비스
- ebcli를 이용한 커맨드라인 배포 지원
- eb init // 어플리케이션 초기화
- eb create // 서버 환경 생성
- eb deploy // 서버 배포, 최신 커밋 버전을 배포함
코드 스캐폴딩
-
yoeman
- 웹 프레임웍을 이용한 코드 제너레이터
- express, hapi, angular, react 등 코드 제너레이터 제공
-
generator-weplajs
- express를 이용한 api 서버 개발용 코드 제너레이터
- Hapi
- Sails
- Restify
- Meteor
감사합니다.
[OCL] Node.js를 이용한 모바일 API 서버 개발
By 김정환
[OCL] Node.js를 이용한 모바일 API 서버 개발
- 8,024