양재동 코드랩

NodeJS를 통한 Rest API 개발

[제 1회]

슬라이드 :https://slides.com/jeonghwan/nodejs/live

예제코드 : https://github.com/jeonghwan-kim/express-ocl

 

NodeJS

노드JS 구조

  • 어플리케이션/모듈: 확인할 수 있는 모든 자바스크립트 코드
  • 바인딩 레이어: JS와 C/C++ 코드를 묶는 역할
  • libuv: 비동기 기능을 제공하는 C 라이브러리
  • V8: 자바스크립트 엔진

이벤트 기반 비동기 I/O

  • 이벤트 큐에 이벤트 저장
  • 메인 쓰레드: 큐에서 이벤트를 꺼내 실행
    • 비동기 잡이면 일꾼 쓰레드 위임
    • 동기 잡이면 실행
  • 일꾼 쓰레드: 잡을 완료하고 큐로 저장
  • 메인 쓰레드:
    큐에서 완료된 잡을 꺼내 실행

모듈시스템

  • 격리된 모듈을 만들기 위한 클로져 생성 (IIFE)
  • 비동기 모듈 시스템 AMD (RequireJS)
  • 파일 형태의 모듈 시스템 CommonJS (NodeJS)
// 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));

ECMAScript 6

자바스크립트의 새로운 스펙인 ES6에 대해서

let, const

블록스코프를 따르는 변수 키워드 (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 => ...);

Hello world

NodeJS로 헬로 월드 코드를 만들어 보자

Hello world

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}/`);
});
  • node --version
  • npm --version
  • node app.js
  • npm init
  • npm start
  • curl -X GET localhost:3000

자주 사용하는 명령어

// packageg.json

{
  "scripts": {
    "start": "node app.js"
  }
}

익스프레스JS

노드 웹 프레임워크 ExpressJS

Hello world

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와 Http

  • 요청 처리
    • 쿼리문자열 파싱
    • 리퀘스트 바디 파싱 (body-parser)
  • 응답 처리
    • 응답 헤더 자동 설정
    • 응답 형식에 따른 메소드 지원 
  • 라우팅 처리
    • 구조적인 코드를 유지할 수 있음
  • 미들웨어
    • 미들웨어 형태로 기능을 추가함: 로깅(morgan), 인증(passport) ...

어플리케이션

  • 어플리케이션: 익스프레스 인스턴스
  • 서버에 필요한 기능을 미들웨어 형태로 추가할 수 있음 
  • 라우팅 설정 
  • 요청 대기 상태 (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'));

요청 객체

  • 클라이언트 요청 정보를 담은 객체 
    • 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'}
});

응답 객체

  • 클라이언트 응답 정보를 담은 객체 
    • http 모듈의 response 객체를 사용함
  • express에서 아래 객체를 추가함 
    • res.send(): 문자열로 응답 
    • res.status(): HTTP 상태 코드를 헤더에 설정 
    • res.json(): 제이슨 데이터를 응답
app.get('/:id', (req, res) => {
    res.status(204).end();
    res.send('hello world');
    res.json([{msg: 'hello world'}]);
});

라우터

  • 라우팅 설정을 위한 클래스
  • API 서버 핵심인 라우팅 로직을 구조적으로 구현할 수 있음 
/* 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'

미들웨어

  • 익스프레스 미들웨어(링크)
    • body-parser: 요청 바디 데이터 파싱
    • morgan: 서버 콘솔 로그 출력
    • passport: 인증 처리
  • 사용자 정의 미들웨어 
    • 기본적인 미들웨어 
      • 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()

REST API

요청

모든 자원은 명사 단어로 식별함

  • /users/{id} vs /getUserById

자원에 대한 행동은 메소드로 표현

  • GET /users/{id}
  • CRUD: GET, POST, PUT, DELETE

응답

헤더는 상태코드로 성공/실패를 표현

  • 200: 성공(success)
  • 201: 작성됨(created)
  • 204: 내용 없음 (No Conent)
  • 400: 잘못된 요청 (Bad Request)
  • 401: 권한 없음 (Unauthorized)
  • 404: 찾을 수 없음 (Not found)
  • 409: 충돌 (Conflict)
  • 500: 서버 에러 (Interel server error)

본문(Body)은 JSON 형식으로 표현

 
[{
  id: 1, 
  name: 'Alice'
}, {
  id: 2, 
  name: 'Bek'
}, {
  id: 3, 
  name: 'Chris'
}]

GET /users

사용자 목록 조회

// 임시데이터 
let users = [{
  id: 1, 
  name: 'Alice'
}, {
  id: 2, 
  name: 'Bek'
}, {
  id: 3, 
  name: 'Chris'
}];

// 라우팅 설정 
app.get('/users', (req, res) => {
  // 여기에 라우팅 로직을 작성하면 됩니다.
  res.json(users);
});

BDD

Mocha, should, superTest

Mocha

테스트 러너 

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();
  });
});

Should

검증자 

npm i should --save-dev

가독성 높은 코드를 유지할 수 있음

const should = require('should');

describe('test suite', () => {
  it('true is true', (done) => {
    (true).should.be.equal(true);
  });
});

SuperTest

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();
        })
  });
});

GET /users를 TDD로

  • success
    • 유저 객체를 담은 배열을 응답한다
    • 최대 limit 갯수만큼 응답한다
  • error
    • limit이 숫자형이 아니면 400을 응답한다
    • offset이 숫자형이 아니면 400을 응답한다

API 개발

GET /users

      success
        ✓ 유저 객체를 담은 배열을 응답한다
        ✓ 최대 limit 갯수만큼 응답한다
      error
        ✓ limit이 숫자형이 아니면 400을 응답한다
        ✓ offset이 숫자형이 아니면 400을 응답한다

GET /users/:id

      success
        ✓ id가 1인 유저 객체를 반환한다
      error
        ✓ id가 숫자가 아닐경우 400으로 응답한다
        ✓ id로 유저를 찾을수 없을 경우 404로 응답한다

DELETE /users/:id

      success
        ✓ 204를 응답한다
      error
        ✓ id가 숫자가 아닐경우 400으로 응답한다

POST /users

      success
        ✓ 생성된 유저 객체를 반환한다
        ✓ 입력한 name을 반환한다
      error
        ✓ name 파라매터 누락시 400을 반환한다
        ✓ name이 중복일 경우 409를 반환한다

PUT /users/:id

      success
        ✓ 변경된 정보를 응답한다
      error
        ✓ 정수가 아닌 id일 경우 400 응답
        ✓ name이 없을 경우 400 응답
        ✓ 없는 유저일 경우 404 응답
        ✓ 이름이 중복일 경우 409 응답

코드 정리

api/user/index.js

api/user/user.ctrl.js

api/user/user.spec.js

데이터베이스

데이터베이스 종류

SQL

  • 데몬: MySQL, PostgreSQL, Aurora (AWS)
  • 파일: Sqlite

 

NoSQL

  • MongoDB
  • DynamoDB (aws)

 

In Momory DB

  • Redis
  • Memcashed

SQL 쿼리

create database codelab

use codelab;

create table user;

select * from user where id = 1;

insert into user (name, age) values ('chris', 30);

...

ORM

Object Relational Mapping

  • SQL: 시퀄라이즈 (Sequelize)를 사용함
  • No SQL: Mongoose 
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))
};

테스트에 디비 연동

서버 구동과 디비 싱크 로직을 모듈로 분리

  • bin/sync-databbase.js
  • bin/www.js

모카 후커로 디비 연동

  • before()
  • after()
  • beforeEach()
  • afterEach()
// 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

  • sqlite 데이터베이스 연동
  • 로그 숨김

development

  • sqlite 데이터베이스 연동 
  • 모든 로그 출력 

production

  • mysql 데이터베이스 연동
  • 최소 로그만 출력 

 

NODE_ENV 환경변수로 식별 

문서화

깃헙 위키 -> APIDOC ->  스웨거

깃헙위키

  • 문서 일관성 유지 어려움
  • FE개발자도 포스트맨으로 테스트하며 개발
  • POST /users
  • input
    • name
    • gender
    • mobile
    • nationality
    • dob
    • ...
  • response
    • userId
    • name
    • gender
    • age
    • address
    • token
    • channel
    • ...

APIDOC

  • 코드에 주석으로 API 문서를 작성하는 형식
  • 코드보다 주석이 많은 경우가 빈번함 

스웨거

  • 스웨거 문법에 맞춰 문서를 작성함 (json, yaml 형식)
  • swagger-ui를 api 서버에 연동하여 개발 
  • 문서와 api 테스트 툴을 함께 사용할 수 있는 장점 

Swagger spec

스웨거 문법에 맞게 문서 작성(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"
  }
}

Swagger spec

작성한 문서를 호스팅 


const swaggerDoc = 
    require('./config/document.swagger');

app.get('/doc', (req, res) => {
  res.json(swaggerDoc);
});

Swagger-ui

스웨거 문서를 렌더링할 swagger-ui 툴을 프로젝트에 추가

app.use('/', (req, res, next) => {
  if (req.url === '/') res.redirect('?url=/doc');
  else next();
}, express.static('node_modules/swagger-ui/dist'));

Swagger-ui

루트 경로로 접근하여 스웨거 문서를 열람 

배포

AWS EC2, Elastic Beanstalk

리눅스 배포

Git hooks을 이용한 배포 자동화

  • 배포서버에 git 저장소 설치
    • git init --bare --shared
  • post-receive 후커 작성
  • 배포서버 리모트 추가
    • git remote add deploy [배포서버주소:저장소경로]
  • 배포서버로 소스코드 배포
    • git push deploy master
# 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

ElasticBeanstalk

주요 AWS 서비스를 묶은 서비스

깃과 자동으로 연동되어 있음.

eb-cli를 통해 더욱 단순하게 배포 가능 

 

ebcli 설치

IAM 유저 생성

eb init

eb create 

eb deploy 

[양재동 코드랩] NodeJS를 통한 REST API 개발

By 김정환

[양재동 코드랩] NodeJS를 통한 REST API 개발

  • 3,357