슬라이드 :https://slides.com/jeonghwan/nodejs/live
예제코드 : https://github.com/jeonghwan-kim/express-ocl
// IIFE
(function ($) {
// $ 는 이 함수 스콥에서만 사용
})($)
// RequireJS(AMD)
<script type="text/javascript">.
require(["jquery.min.js"], function($) {
// $ 는 이 함수 스콥에서만 사용
});
</script>
// CommonJS
// sum.js 파일이 모듈로 감춰짐
const sum = require('./sum.js');
console.log(circle(1, 2));
블록스코프를 따르는 변수 키워드 (let: 변수, const: 상수)
function f() {
{
let x;
{
const x = "sneaky";
// 에러. 상수는 한 번만 할당
x = "foo";
}
// 에러. 블록에서 이미 선언됨
let x = "inner";
}
}
템플릿 문자열을 이용하면 손쉽게 문자열을 만들수 있음
// 기본적인 문자열 생성
`In JavaScript is \n a line-feed.`
// 여러줄 문자열
`In JavaScript this is
not legal.`
// 문자열 인터폴레이션
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
'=>' 문법을 이용해 함수를 짧게 표현한다
arr.map(v => v + 1);
arr.map((v, i) => v + i);
arr.map(v => ({even: v, odd: v + 1}));
어휘적 this를 사용하기 때문에 동적 this의 모호함을 개선한다.
// 함수 표현식은 동적 this
var numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // true
function calculate() {
// this는 window, 엄격 모드였으면 undefined
console.log(this === numbers); // false
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // NaN, 엄격 모드였으면 TypeError
// 화살표 함수는 어휘적 this
var numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // true
const calculate = () => {
console.log(this === numbers); // true
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // 10
콜백 스타일의 비동기 코드는 콜백에 로직의 순서가 매여있음
프라미스는 비동기 코드를 값으로 만들어줌
비동기 코드 콜백의 주도권을 제어함
프라미스 체인을 이용해 코드를 평탄화 함
// 프라미스
const foo = new Promise(resolve => {
console.log(0);
setTimeout(_=> resolve(2), 10);
});
setTimeout(_=> {
console.log(1);
foo.then(n => console.log(n))
}, 100);
// 0, 1, 2
// 프라미스
const foo = new Promise(...);
const bar = new Promise(...);
Promise.resolve()
.then(foo)
.then(bar)
.then(val => ...);
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(`Server running at http://${hostname}:${port}/`);
});
// packageg.json
{
"scripts": {
"start": "node app.js"
}
}
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`)
});
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'));
app.get('/:id', (req, res) => {
const id = req.params.id; // /1
const limit = req.query.limit; // ?limit=10
const body = req.body; // {name: 'chris'}
});
app.get('/:id', (req, res) => {
res.status(204).end();
res.send('hello world');
res.json([{msg: 'hello world'}]);
});
/* index.js */
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;
미들웨어 호출 순서
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'));
'Run server'
'middleware 1'
'middleware 2'
'middleware 3'
'router Get /'
에러 미들웨어: 에러를 처리한 경우
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'));
'Run server'
'middleware 1'
'middleware 2 error 1'
'middleware 3'
'router Get /'
에러 미들웨어: next(error)로 에러를 위임한 경우
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'));
'Run server'
'middleware 1'
'middleware 2 error 1'
'error1'
app.use((req,res,next)=> next())
app.use((err, req,res,next)=> next())
let arr = [1,2,3];
for (const i=0; i<arr.length; i++) {
arr[i] = arr[i] * 2;
}
console.log(arr) // [2,4,6]
let arr = [1,2,3];
arr = arr.map(num => num * 2);
console.log(arr) // [2,4,6]
반복문: for
배열 메소드: map()
map 함수의 내부를 들여다 보면
Array.prototype.map = function(iteratee) {
var results = [];
for (var index = 0; index < this.length; index++) {
results[index] = iteratee(this[index]);
}
return results;
};
var list = [1,2,3];
list.map(function (n) {
return n * 2;
});
reduce()
var sum = [0, 1, 2, 3].reduce(function(a, b) {
return a + b;
}, 0);
// sum is 6
["a", "b", "c"].forEach(function(element) {
console.log(element); // a b c
});
function isBigEnough(value) {
return value >= 10;
}
var filtered = [12, 5, 8, 130, 44].filter(isBigEnough);
// filtered is [12, 130, 44]
forEach()
filter()
모든 자원은 명사 단어로 식별함
자원에 대한 행동은 메소드로 표현
헤더는 상태코드로 성공/실패를 표현
본문(Body)은 JSON 형식으로 표현
[{
id: 1,
name: 'Alice'
}, {
id: 2,
name: 'Bek'
}, {
id: 3,
name: 'Chris'
}]
사용자 목록 조회
// 임시데이터
let users = [{
id: 1,
name: 'Alice'
}, {
id: 2,
name: 'Bek'
}, {
id: 3,
name: 'Chris'
}];
// 라우팅 설정
app.get('/users', (req, res) => {
// 여기에 라우팅 로직을 작성하면 됩니다.
res.json(users);
});
테스트 러너
npm i mocha --save-dev
Test suite: describe()
Test: it()
user.spec.js
node_modules/.bin/mocha user.spec.js
비동기 테스트는 done 콜백을 수행함
const assert = require('assert');
describe('test suite', () => {
it('true is true', done => {
assert.equal(true, true);
done();
});
});
검증자
npm i should --save-dev
가독성 높은 코드를 유지할 수 있음
const should = require('should');
describe('test suite', () => {
it('true is true', (done) => {
(true).should.be.equal(true);
});
});
HTTP 테스트를 위한 용도
npm i supertest --save-dev
module.exports = app;
const request = require('supertest');
const app = require('../../app');
describe('GET /users', () => {
it('should return 200 status code',done=> {
request(app)
.get('/users')
.expect(200)
.end((err, res) => {
if (err) throw err;
console.log(res.body);
// [{id: 1, name: 'Alice'} ... ]
res.body.should.be.instanceof(Array)
res.body.should.have.properties('id', 'name');
done();
})
});
});
success
✓ 유저 객체를 담은 배열을 응답한다
✓ 최대 limit 갯수만큼 응답한다
error
✓ limit이 숫자형이 아니면 400을 응답한다
✓ offset이 숫자형이 아니면 400을 응답한다
success
✓ id가 1인 유저 객체를 반환한다
error
✓ id가 숫자가 아닐경우 400으로 응답한다
✓ id로 유저를 찾을수 없을 경우 404로 응답한다
success
✓ 204를 응답한다
error
✓ id가 숫자가 아닐경우 400으로 응답한다
success
✓ 생성된 유저 객체를 반환한다
✓ 입력한 name을 반환한다
error
✓ name 파라매터 누락시 400을 반환한다
✓ name이 중복일 경우 409를 반환한다
success
✓ 변경된 정보를 응답한다
error
✓ 정수가 아닌 id일 경우 400 응답
✓ name이 없을 경우 400 응답
✓ 없는 유저일 경우 404 응답
✓ 이름이 중복일 경우 409 응답
api/user/index.js
api/user/user.ctrl.js
api/user/user.spec.js
SQL
NoSQL
In Momory DB
create database codelab
use codelab;
create table user;
select * from user where id = 1;
insert into user (name, age) values ('chris', 30);
...
Object Relational Mapping
ORM (Model) | SQL (Table) |
---|---|
find() | select * from |
create() | insert into |
update() | update from |
destroy() | delete from |
시퀄라이즈 define() 메소드로 테이블과 연결된 모델을 정의함
// models.js
const Sequelize = require('sequelize');
const sequelize = new Sequelize('node_api_codelab', 'root', 'root')
const User = sequelize.define('user', {
name: Sequelize.STRING
});
module.exports = {sequelize, User};
정의한 모델을 데이터베이스와 동기화 (쿼리 생성)
// app.js
app.listen(3000, () => {
require('./models').sequelize.sync({force: true})
.then(() => console.log('Databases sync'));
});
모델을 정의한 models.js를 컨트롤러에 임포트
모델 메소드: create(), findAll(), findOne(), update(), destroy()
const models = require('../../models');
exports.create = (req, res) => {
const name = req.body.name || '';
if (!name.length) {
return res.status(400).json({error: 'Incorrenct name'});
}
models.User.create({
name: name
}).then((user) => res.status(201).json(user))
};
서버 구동과 디비 싱크 로직을 모듈로 분리
모카 후커로 디비 연동
// bind/sync-database.js
const models = require('../models');
module.exports = () => {
return models.sequelize.sync({force: true})
}
// /bin/www.js
const app = require('../app');
const syncDatabase = require('./sync-database');
app.listen(3000, () => {
syncDatabase().then(() => {
console.log('Database sync');
});
});
before('sync database', (done) => {
syncDatabase().then(() => done());
});
before('insert seed user data', (done) => {
models.User.bulkCreate(users).then(() => done());
})
after('delete seed user data', (done) => {
models.User.destroy({
where: {
name: {
in: users.map(user => user.name)
}
}
});
});
test
development
production
NODE_ENV 환경변수로 식별
스웨거 문법에 맞게 문서 작성(swagger spec)
const paths = {
'/users': {
get: {
tags: ['User'],
summary: '유저 조회',
operationId: 'findUsers',
consumes: ["application/json"],
produces: ["application/json"],
parameters: [
{
in: "query",
name: "limit",
required: true,
type: 'number',
default: 10
},
{
in: "query",
name: "offset",
required: true,
type: 'number',
default: 0
},
],
responses: {
200: {description: 'OK'},
400: {description: 'BadRequest'}
}
},
post: {
tags: ['User'],
summary: '유저 추가',
operationId: 'addUsers',
consumes: ["application/json"],
produces: ["application/json"],
parameters: [
{
in: "body",
name: "body",
required: true,
schema: {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
},
],
responses: {
201: {description: 'Created'},
400: {description: 'BadRequest'},
409: {description: 'Conflict'}
}
}
},
'/users/{id}': {
get: {
tags: ['User'],
summary: '유저 조회',
operationId: 'getUsers',
consumes: ["application/json"],
produces: ["application/json"],
parameters: [
{
in: "path",
name: "id",
required: true,
type: 'number'
},
],
responses: {
200: {description: 'NoContent'},
404: {description: 'BadRequest'}
}
},
delete: {
tags: ['User'],
summary: '유저 삭제',
operationId: 'delUsers',
consumes: ["application/json"],
produces: ["application/json"],
parameters: [
{
in: "path",
name: "id",
required: true,
type: 'number'
},
],
responses: {
204: {description: 'NoContent'},
400: {description: 'BadRequest'},
404: {description: 'BadRequest'}
}
},
put: {
tags: ['User'],
summary: '유저 수정',
operationId: 'putUsers',
consumes: ["application/json"],
produces: ["application/json"],
parameters: [
{
in: "path",
name: "id",
required: true,
type: 'number'
},
{
in: "body",
name: "body",
required: true,
schema: {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
},
],
responses: {
200: {description: 'Created'},
400: {description: 'BadRequest'},
409: {description: 'Conflict'}
}
}
}
};
module.exports = {
"swagger": "2.0",
"info": {
"description": "codelab api 문서입니다",
"version": "1.0.0",
"title": "Swagger codelab api document",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"email": "apiteam@swagger.io"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
}
},
"host": "localhost:3000",
"basePath": "/",
"schemes": [
"http"
],
"paths": paths,
"securityDefinitions": {
"petstore_auth": {
"type": "oauth2",
"authorizationUrl": "http://petstore.swagger.io/oauth/dialog",
"flow": "implicit",
"scopes": {
"write:pets": "modify pets in your account",
"read:pets": "read your pets"
}
},
"api_key": {
"type": "apiKey",
"name": "api_key",
"in": "header"
}
},
"externalDocs": {
"description": "Find out more about Swagger",
"url": "http://swagger.io"
}
}
작성한 문서를 호스팅
const swaggerDoc =
require('./config/document.swagger');
app.get('/doc', (req, res) => {
res.json(swaggerDoc);
});
스웨거 문서를 렌더링할 swagger-ui 툴을 프로젝트에 추가
app.use('/', (req, res, next) => {
if (req.url === '/') res.redirect('?url=/doc');
else next();
}, express.static('node_modules/swagger-ui/dist'));
루트 경로로 접근하여 스웨거 문서를 열람
Git hooks을 이용한 배포 자동화
# hooks/post-receive
#!/bin/bash
APP_NAME=codelab_nodeapi
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
npm install --production
forever stop codelab_nodeapi
forever start --uid codelab_nodeapi --append bin/www.js
주요 AWS 서비스를 묶은 서비스
깃과 자동으로 연동되어 있음.
eb-cli를 통해 더욱 단순하게 배포 가능
IAM 유저 생성
eb init
eb create
eb deploy