08/19, refactoring server with express

Ian·2020년 8월 19일
0

Today I Learned

목록 보기
2/40
post-thumbnail

HTTP, 다시 짚고 넘어가보기

  • 먼저, HTTP 를 통해 주고받는 자료형은 event emitter 중 'data' type 의 event emitter 는 주고받는 데이터가 string혹은 buffer 여야 한다.
  • 그렇기에 ObjectArray 등의 자료형을 바로 보내지 않고, JSON.stringify(arguments) 를 통해 string 을 만들어서 자료를 보내주는 것이다.

JSON 관련 메소드 정리

  • JSON.stringify(arg)

인자로 들어간 argJSON 타입으로 변화시켜주는 메서드이다.

let obj = {
    "key1" : 1,
    "key2" : 2
}
//{key1: 1, key2: 2}, this type is object

JSON.stringify(obj)
//"{"key1":1,"key2":2}", this type is string (JSON)
  • JSON.parse(arg)

인자로 들어간 arg 를 읽어내서, 그 값으로부터 JS 값이나 Object 를 생성한다

let JSONized = JSON.stringify(obj)
//"{"key1":1,"key2":2}", this type is string (JSON)

JSON.parse(JSONized)
//{key1: 1, key2: 2}, this type is Object

Common JS

module 과 관련이 있는 영역. Common JS 의 원칙은 다음과 같다.

  • 모든 모듈은 자신만의 독립적인 실행영역이 있어야 한다.
  • 그를 위해 전역객체인 exports를 이용하여 내보낸다.
  • 그리고 그렇게 내보낸 무언가를 require함수를 이용한다.

module.exports vs export 의 차이점은 무엇인가

우선 기능의 차이는 없다. 그리고 이 둘의 관계는 원어-축약어와 같다고 볼 수 있는데, exportmodule.exports 의 축약어이다. 원래는 module.exports라는 것이 먼저 존재했었다. 그러나 그렇게 길게 쓰기 싫었기에, 그것의 shortcut 과도 같은 느낌으로 exports 가 나오게 된 것이다.

그러나, 앞서 말 했듯 엄연히 원조는 module.exports, exports 는 그 module.exports참조하는 것 이기에 둘을 혼용해서 사용하면 절대로 안 된다.

//hello.js file
module.exports = {a: 1};

//hello-runner.js file
const hello = require('/hello');
console.log(hello); // {a: 1} 이 출력됨

이 경우는 moduleexport 객체에 {a: 1}이라는 객체가 할당이 잘 된다. 그래서 그것을 require 을 통해 불러오게 되면 module.exports 객체에서 {a: 1} 이라는 객체를 잘 불러온다.

//hello.js file
exports = {a: 1};

//hello-runner.js file
const hello = require('/hello');
console.log(hello); // 빈 객체가 출력됨 

그러나 이 경우는 exports 라는 '변수' 에 {a: 1}이라는 객체가 할당이 되는 것이기에, require을 통해서 값을 불러올 수 없게된다.

만약 우리가 export 자체에 어떤 객체를 넣어서 내보내주고 싶다면, exports 가 아닌 export를, 그리고 require 가 아닌 import 를 사용해줘야 한다. 문법 자체가 판이하게 다르므로 사용방법이 다르다는 사실을 꼭 이해하도록 하자.

//hello.js file
export = {a: 1};

//hello-runner.js file
const hello = import * as myModule from "/hello.js";
console.log(hello); // 빈 객체가 출력됨 

그리고 node.jsCommonJS 를 사용하고 있는 런타임이기 때문에 export-import 를 사용할 수 없다.

  • 관련문서

CommonJS 의 역사

NAVER D2

MDN-export, import 문서

export

import


Office Hour

module.exports.var 를 먼저 할당하고, 그 다음라인에 export.var 나중에 할당해도 module.exports.var 는 변하지 않는다. 우선순위 자체가 module.exports.var 가 먼저이다.

//export.js file
exports = 10;
module.exports = 20;

//import.js file
const exportedVar = require('./hello');
console.log(exportedVar);

require(./fileDir) 를 통해 어떤 값을 불러온다는 것은, require를 실행한 .js 파일에서, fileDir 이라는 JS 의 코드라인 자체를 붙여넣는다고 생각하면 된다.

//counter.js
let counter = 0;
exports.increment = function() {
	counter += 1;
	return counter;
};

/*---------------------------------------------*/

//usingCounter.js
let mod1 = require('./counter');
let mod2 = requrie('./counter');

mod1.increment();
let result = mod2.increment();

console.log(result);

위에 말한 것처럼, mod1mod2 는 결국 counter.js 라는 하나의 파일에서 같은 코드라인을 가져온 것과 같다. 즉, 참조이다.

let obj = {} // 위의 counter.js 내용과 같은 객체라고 치면...

mod1 = obj;
mod2 = obj;

와 같은 상황이다.

그렇기 때문에 counter 는 누산이 된다. mod1.increment() 한 번 호출되었으니 0→1, 그리고 mod2.increment 를 통해 한 번 더 호출이 되었으니 1→2 가 되고 그 값을 result 에 할당했기 때문에 result 의 값은 2가 담긴다.

EVENT LOOP

sync code start → async code start first in first out

이 순서대로 실행이 된다.

예제를 보면서 이해해보자

console.log("A");

setTimeout(() => {
	console.log("B")
}, 1000);

setTimeout(() => {
	console.log("C")
}, 500);

console.log("D");

//A, D, C, B 순서대로 콘솔에 찍힌다. 
//비동기 함수는 콜스택에 들어가고 그 콜스택에 들어간 순서대로 실행된다 
console.log("A");

setTimeout(() => {
	console.log("B")
}, 0);

console.log("C");

//이 경우는 A, C, B 순으로 실행이 된다
//이유는, 어쨌든 setTimeout 의 time arg가 얼마가 되든간에 동기 -> 비동기 순으로 출력이 되기 때문이다.

동기처리 어떤 연산들이 아무리 오래걸려도, 동기→비동기 처리순서라는 대원칙은 변하지 않는다.

그렇기 때문에 밑과 같은 코드도 A, C, B 순서대로 똑같이 콘솔에 찍힌다.

console.log("A");

setTimeout(() => {
	console.log("B")
}, 0);

syncFunctionSuperLongComputation() // 해당 함수는 동기함수로, 연산완료까지 약 5초가 걸린다고 치자.

console.log("C");

이 경우도 마찬가지로 A, C, B 순으로 실행이 된다. 순서를 조금 더 명시적으로 적어보자면

  1. A를 콘솔에 출력
  2. setTimeout 함수가 event loop 의 queue 에 담김
  3. syncFunctionSuperLongComputation() 을 실행
  4. C를 콘솔에 출력, 모든 동기함수들이 처리완료되어 콜스택이 비어있음
  5. 콜스택이 비어 event loop 의 queue 에서 setTimeout 이 실행됨, 그러면서 B가 콘솔에 출력된다.
  6. event loop queue 까지 전부 빔, 모든 함수가 중료된다.
console.log("A");

setTimeout(() => {
	console.log("B")
}, 1000);

syncFunctionSuperLongComputation() // 해당 함수는 동기함수로, 연산완료까지 약 5초가 걸린다고 치자.

setTimeout(() => {
	console.log("C")
}, 500);

console.log("D");

//A, D, B, C 순으로 출력된다.

병준님의 설명 : WebApi에서 B를 호출하는 구문이 1초를 delay재고있을때, superLogComputation이 동기적으로 CallStack에서 뻐기고 있기때문에 C를 호출하는 구문이 실행전에 이미 B는 Callback Queue로 가서 먼저 대기 하기때문에라고 생각하시면됩니다.

혁원님의 설명 : superLongComputation에서 코드가 막혀서 C를 출력하는 setTimeout 자체가 호출이 안된 경우입니다.setTimeout 자체가 호출이 안되서 C를 출력하는 함수가 콜스택에 들어가지를 못합니다

console.log("A");

setTimeout(() => {
	console.log("B")
}, 500);

setTimeout(console.log("C"), 1000);

//A, C, B

이 경우는 setTimeout 을 이용한 트릭이다. setTimeout 의 함수 인자에 함수를 "실행" 하는 형식으로 넣는다면, setTimeout은 비동기함수에서 동기함수로 바뀌어 실행이 된다. 그렇기 때문에 A, C, B 순서대로 실행이 된다. 왜냐, "C" 를 출력하는 setTimeout은 동기함수가 되었기 때문이다.

console.log("A");

setTimeout(go, 500);

setTimeout(console.log("B"), 1000);

function go() {
	console.log("X");
}

//A, B, X

아까의 트릭에 몇 개를 추가한 방식이다. A는 동기함수라 먼저 실행이 되고, setTimeout(console.log("B"), 1000); 도 동기함수가 되어버려 B가 출력한다. 그 다음으로는 비동기적인 성격이 살아있는 setTimeout(go, 500); 가 실행되어 X 가 출력된다.


express.js refactoring

SPEC for express.js server

  • handling POST, GET request to /message url
  • handling error
  • seperate router .js file from server running .js file

express 는 node 를 통해 조금 더 서버 작성을 쉽게 만들어 주는 라이브러리이다. 특히 body-parsercors 등으로 기존에 여러 줄의 코드를 입력해 줘야했던 설정들을 간단하게 만들어준다는 장점이 있다.

  1. 기본적인 서버의 구현
//기본적인 서버의 구현 
const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

출처 : express.js 웹페이지의 "hello world" 예제

Express "Hello World" 예제

모든 주소로 오는 GET 요청에 대해서 "Hello World" 라는 응답을 보내주는 서버를 만들었다. 이렇게 아주 간단하게 서버를 구현할 수 있다. node.js 에 비해서 훨씬 간단하다.

middleware of express.js

Middleware literally means anything you put in the middle of one layer of the software and another. Express middleware are functions that execute during the lifecycle of a request to the Express server. Each middleware has access to the HTTP request and response for each route (or path) it’s attached to. In fact, Express itself is compromised wholly of middleware functions. Additionally, middleware can either terminate the HTTP request or pass it on to another middleware function using next (more on that soon). This “chaining” of middleware allows you to compartmentalize your code and create reusable middleware.

출처 : okta.com

Build and Understand Express Middleware through Examples

미들웨어는 간략하게 설명하자면, express 에서 request 를 처리하는 과정에 추가하는 또 다른 과정들을 의미한다. .use 를 통해서 사용할 수 있으며, 대표적인 미들웨어로는 body-parsercors 가 있다.

위에 나온 body-parsercors 를 통해, request 를 조금 더 간편하게 다뤄보자.

//example of body-parser's json parser
const express = require('express')
const app = express();

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

app.use(bodyParser.json());

app.get('/', (res, req) => {
	console.log(res.body);
});

기존의 node.jshttp 모듈을 사용한 서버에서 request 에 있는 body 의 데이터에 접근하기 위해서는 Buffer 로 날아오는 reques 의 data 를 하나로 합쳐주고, 그것을 toString() 으로 문자열을 변환하여 그로부터 의미있는 결과물을 읽어내기 위해 JSON.parse() 를 이용해야 했었다.

그러나 express 의 body-parser , 그 중에서 json parser 를 이용한다면, 그 문자열을 res.body 라는 방식으로 더욱 쉽게 받을 수 있게 되었다.

cors 를 이용해 CORS 를 구현해 주는 것도 훨씬 간단하다.

//example of cors
const app = express();
const cors = require('cors');

app.use(cors());
app.options('*', cors());
//allow CORS from all client

기존에는 cors 세팅을 잡아준 header 를 일일히 설정해 주고, response.writeHead 를 통해 header 를 response 에 할당해줘야 했다.

그러나 express 의 cors 모듈을 이용한다면 이런 식으로 더욱 간단하게 CORS 세팅이 가능하다.

app.route() 및 Router() 를 이용한 라우팅

app.route()를 이용하면 라우트 경로에 대하여 체인 가능한 라우트 핸들러를 작성할 수 있습니다. 경로는 한 곳에 지정되어 있으므로, 모듈식 라우트를 작성하면 중복성과 오타가 감소하여 도움이 됩니다. 라우트에 대한 자세한 정보는 Router() 문서를 참조하십시오.

출처 : express.js 의 express 라우팅 문서의 app.route() 부분

라우팅

app.get('/endPointA', (res, req) => {
	console.log(res.body);
});

app.post('/endPointA', (res, req) => {
	console.log(res.body);
});

app.put('/endPointA', (res, req) => {
	console.log(res.body);
});

app.delete('/endPointA', (res, req) => {
	console.log(res.body);
});

물론 이런 식으로도 라우팅이 가능하다. 그러나 만약 한 endpoint 에 여러가지 HTML method 들을 라우팅 해주어야 하는 경우, 이런 식으로 계속해서 만들어 나가는 것은 비효율적이다. 이를 위해 존재하는 것이 app.route 이다. app.route()를 이용하여 조금 더 효율적인 라우팅을 할 수 있다.

app.route('/endPointA')
  .get((req, res) => {
    res.send('Get a random book');
  })
  .post((req, res) => {
    res.send('Add a book');
  })
  .put((req, res) => {
    res.send('Update the book');
  });

express 4.X 버전의 API 문서에 나와있는 Router 객체도 활용해 볼 수 있다.

4.x API

var router = express.Router();
// routing section 
// invoked for any requests passed to this router
router.use((req, res, next) => {
  // .. some logic here .. like any other middleware
  next()
})

// will handle any request that ends in /events
// depends on where the router is "use()'d"
router.get('/events', function (req, res, next) {
  // ..
})

// using router
// only requests to /calendar/* will be sent to our "router"
app.use('/calendar', router)

이런 식으로 express.Router() 객체를 router 라는 변수에 할당해 준 뒤, 그 router변수에서 routing 을 진행해 주고, 그렇게 만들어진 router 변수를 app.use('/endpoint', router) 를 통해 활용하는 것이 가능하다.

해당 방식은 routing 을 진행하는 .js 파일을 따로 분리시키는 것이 가능하여, 파일단위로 모듈화를 가능하게 한다는 장점이 있다.

error handling

다른 미들웨어 함수와 똑같은 방식으로 오류 처리 함수를 정의할 수 있으나, 오류 처리 함수는 함수의 인자가 3개가 아닌 4개를 가진다. 또한 해당 미들웨어는 다른 app.use() 및 route 호출을 모두 정의하고 난 뒤 가장 마지막으로 정의해야 한다.

// new argument 'err' in error handling middleware function 
app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

출처 : express.js - 오류 처리

오류 처리

profile
правда и красота, truth and beauty

0개의 댓글