이전 Sprint에서는 Node.js를 이용한 서버 구축을 해보았습니다. 그러나 코드가 복잡해지면 이렇게 계속 if문이 계속 되는 코드가 매우 힘들어질 것입니다.
이런 상황에서 코드의 복잡성을 낮춰주고, 웹 애플리케이션을 구현하는 과정에서 공통적으로 요구되는 일들을 대신해주는 것이 프래임워크라고 합니다.
https://wikibook.co.kr/article/what-is-expressjs/에서는
Node.js의 핵심 모듈만 이용해서 중요 앱을 작성한다면
HTTP 요청 본문 파싱
쿠키 파싱
세션 관리
URL 경로와 HTTP 요청 메서드를 기반으로 한 복잡한 if 조건을 통해 라우팅을 구성
데이터 타입을 토대로 한 적절한 응답 헤더 결정
하는 과정을 지속적으로 반복해야 하는 반면,
Express.js는 이러한 문제를 비롯해 다른 여러 문제를 해결함과 동시에 웹 앱에 MVC 형태의 구조를 제공한다. 이 같은 앱은 백엔드만 갖춘 REST API에서부터 온갖 기능을 제공하는 고도로 확장 가능한 풀스택 실시간 웹 앱에 이르기까지 다양하다. 고 합니다.
이번에는 Node.js를 이용한 서버 구축을 Node.js의 핵심 모듈인 http와 Connect 컴포넌트를 기반으로하는 Node.js 대표 웹 프래임워크인 Express를 이용해서 리펙토링해보았고, 그 과정에서 Express에 대해 배운 것을 정리하고자 합니다.
1. Express 설치
http://expressjs.com/en/starter/installing.html
설치를 하면 node_modules안에 express가 추가된 것을 볼 수 있습니다.
2. Express - Getting Started - Hello world example
여기서 Express의 가장 기본이 되는 구조를 살펴보면 다음과 같습니다.
http://expressjs.com/en/starter/hello-world.html
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello World!')
})
const port = 3000
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
이렇게 치고 저장 후 서버를 구동시키면 화면에 'Hello World!'가 찍혀있는 것을 볼 수 있다.
각 줄의 코드가 무슨 의미이길래?!
① express 모듈과 Application이라는 객체 생성
const express = require('express') //'express'모듈을 가져와서
const app = express() //express를 함수처럼 호출한다. 그리고 그 함수의 리턴값은 app이라는 변수에 담겨있다.
app에 담긴 리턴값은 뭐냐면, Application이라는 객체이다.
Express 공식 홈페이지에 가면 이 Application이라는 객체의 생성과 그 객체에 대한 설명, method들이 잘 나와있다.
② Application의 method 중 하나인 get
app.get('/', (req, res) => {
res.send('Hello World!')
})
get method 사용법을 보니 app.get(경로, 그 경로가 호출되었을 때(그 경로로 접속했을 때) 호출될 callback함수)로 되어있다.
※ '경로' 가 나왔다. 여기서 Routing(Route) 의 개념이 필요하다.
사용자들이 각각의 Path에서 어떤 요청을 보냈을 때 그 Path에 맞는 응답을 해 주어야하기 때문에 '경로'인자가 들어가는 것이고, 이에 맞게 상이한 알맞은 callback함수를 넣어주는 것이다.
※ Express를 사용하지 않았을 때는 if 문을 통해서 어떤 경로일 때 무슨 callback이 실행되어야하는지 적어두었었다.
const requestHandler = function (request, response) {
if(request.method === 'GET'){
if(request.url === '/messages'){
response.writeHead(200, headers);
response.end(JSON.stringify(results));
}
else{
response.writeHead(404, headers);
response.end();
}
}
};
그러나
※ Express를 사용 했을 때는 아래와 같이 쓰면 된다.
/*
express.Router()의 의미
: 이를 이용해서 router로 분리할 수 있다. (이전처럼 if문을 복잡하게 쓰지 않아도 된다.)\
이렇게 분리할 수 있게 만든 router는 module.export를 통해 모듈로 만들어지고,
이를 통해 다른 파일에서 require하여 사용할 수 있다.
*/
const router = express.Router()
router.get('/messages', cors(corsOptions), (req, res) => {
res.send(results.results)
})
if 의 사용 유무 외에도 또 차이점은
response 순서가
※ Express를 사용하지 않았을 때
response.writeHead(200, headers);
response.end(JSON.stringify(results));
※ Express를 사용 했을 때
response.send(results.results)
한 줄로 된다는 것이다. response.send(object)로 코드를 실행했을 때 함수의 실행 순서는 response.send(object)-> response.json(object)->response.send(string)이다. 그래서 굳이 JSON.stringify를 해주지 않아도 되는 것이다.
③ 웹서버 실행
const port = 3000
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
3. Routing
Routing은 특정URI 및 특정HTTP 요청 메소드(GET, POST 등) 에 대해 애플리케이션이 응답하는 방법을 결정하는 것을 말한다.
위의
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello World!')
})
const port = 3000
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
이 코드는 app.get('/', 콜백) 이기 때문에 'http:localhost:8080/' 즉 home일때 실행되는 코드이다.
그런데 내가 만든 client는 메뉴가 다양하고 그 메뉴를 눌러서 해당 페이지에 들어가면 ''http:localhost:8080/about' 이나 ''http:localhost:8080/project'..등 다양한 경로가 있다면??
그때 필요한 것이 Route Parameter이다.
이 내 상세 경로(메뉴 중 하나를 클릭해서 들어간 창의 경로)가 들어있는 request.params을 잘 이용한다.
예를 들어 메뉴 중 하나를 클릭해서 들어간 창의 경로가 http:localhost:8080/project라면
아래의 코드의 menuId는 project이고 request.params.menuId도 project이다.
app.get('/:menuId', (request, response) => {
//로 쓰면 되고,
//이 menuId 값을 request.params.menuId에 있다.
})
4. get요청과 post 요청
위에서 살펴본
app.get('/', (req, res) => {
res.send('Hello World!')
})
형식과 동일하다.
또한 if 문을 사용하지 않고도
app.get('/', (req, res) => {
res.send('This is Get!')
})
app.post('/', (req, res) => {
res.send('This is Post!')
})
같은 'http:localhost:8080/' 경로더라도 get요청을 하면 res.send('This is Get') 이 실행될 것이고, post 요청을 하면 res.send('This is Post') 가 실행된다.
※ Express를 사용하지 않았을 때
if (request.method === 'POST') {
if (request.url === '/messages') {
response.writeHead(201, headers)
let message = ''
request
.on('data', (chunk) => {
message += chunk
})
.on('end', () => {
var post = JSON.parse(message)
results.results.push(post)
response.end(JSON.stringify(message))
})
} else {
response.writeHead(404, headers)
response.end()
}
} else if (request.method === 'GET') {
if (request.url === '/messages') {
fs.readFile('data', (err, data) => {
if (err) {
console.log(err)
response.writeHead(404, headers)
}
response.end(JSON.stringify(data))
response.writeHead(200, headers)
})
}
} else {
response.writeHead(404, headers)
response.end()
}
※ Express를 사용 했을 때
router.get('/messages', cors(corsOptions), (req, res) => {
res.send(results.results)
})
router.post('/messages', cors(corsOptions), (req, res) => {
let post = req.body
results.results.push(post)
res.send(results.results)
})
router.use(function (req, res) {
res.status(404).send('Sorry cant find that!')
})
5. Express Middleware
1) Middleware의 사용
http://expressjs.com/en/guide/using-middleware.html#middleware.third-party
Express 공식홈페이지에 나온 설명에 따르면 Express는 기본적으로 일련의 미들웨어 함수의 호출로 구성되어있고,
미들웨어는 요청 객체(req), 응답 객체(res), 다음 미들웨어 함수에 대한 접근권한을 가지는 next함수(그래서 이 next자리엔 그 다음에 호출되어야 할 함수 이름이 담겨있다.) 로 구성되어있다.
즉 미들웨어는 중간 처리 역할을 수행한는 소프트웨어를 의미합니다.
Express에는 use the following types of middleware: 아래의 타입들의 미들웨어가 있다.
- Application-level middleware
- Router-level middleware
- Error-handling middleware
- Built-in middleware
- Third-party middleware
이중 특히 Third-party middleware의 body parser middleware을 사용해보면, body는 웹브라우저제에서 요청한 정보의 본체를 말하고, 이 본체를 설명하는 데이터를 header라고 한다.
이전에 이 정보인 body를 parser해서 우리가 필요한 형태로 가공하기 위해서(parsing(parse)는 가지고 있는 데이터를 내가 원하는 형태의 데이터로 '가공'하는 과정을 말한다.) body-Parser가 특정 문자를 기준으로 파싱하여파싱한 결과 body에 객체 형태로 데이터가 담기고, 그러면 req.body에 이 객체를 저장한다. 그래서 클라이언트 측에서 { name: 'yejinh', job: ...} 와 같은 JSON 형식의 바디를 보내면 서버 측에서 req.body 혹은 req.body.name, req.body.job 등으로 해당 데이터에 곧바로 접근할 수 있게 된다
※ Express를 사용하지 않았을 때
※ Express에서 body parser middleware를 사용 했을 때
const bodyParser = require('body-parser')
router.use(bodyParser.json())
router.post('/messages', cors(corsOptions), (req, res) => {
let post = req.body //var post = JSON.parse(message);
results.results.push(post)
res.send(results.results)
})
이렇게 간결하게 코딩할 수 있다.
2) Middleware의 생성
http://expressjs.com/en/guide/writing-middleware.html
위의 문서에는 Middleware를 만드는 방법이 나와있고, myLogger라는 미들웨어를 만드는 것을 예시로 보여준다.
아래 코드를 보면,
// Middleware function myLogger
// Here is a simple example of a middleware function called “myLogger”
var express = require('express')
var app = express()
var myLogger = function (req, res, next) {
console.log('LOGGED')
next()
}
app.use(myLogger)
app.get('/', function (req, res) {
res.send('Hello World!')
})
app.listen(3000)
app.use를 사용하고 그 인자로 어떤 변수(myLogger)을 준다.
그 변수(myLogger)는 함수네요. var myLogger = function (req, res, next) {
그리고 그 함수의 req, res, next를 인자를 가지고 있다.
여거서 알 수 있듯이 Express에서는 myLogger와 같은 함수형태를 가진 것을 middleware라고 한다. 그리고 그 함수를 어떻게 구현하느냐에 따라서 다른 기능을 하는 Third party middleware가 되는 것이다.
3) Middleware의 실행 순서
미들웨어는 Node와 다르게 순차적으로 실행되기때문에 코드 순서가 매우 중요하다.
여러 middelware 타입 중
- Application-level middleware 을 살펴보면
①
var express = require('express')
var app = express()
app.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})
app.use에 함수를 인자로 넣어주게 되면 이 함수는 middelware로써 등록되고,
middleware의 핵심은 request와 response 객체를 받아서 그것을 변형할 수 있고,
next를 호출함으로써 그 다음에 실행되어야 할 middleware의 실행 유무를 결정한한다.
뿐만아니라
②
app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method)
next()
})
이처럼 app.use에 특정 경로를 인자로 줌으로써 특정 경로일때만 해당 middleware을 실행하게 할 수도 있다.
그리고
③
app.get('/user/:id', function (req, res, next) {
res.send('USER')
})
요청받은 method가 get 방식일때만 해당 middleware을 실행하게 할 수도 있다.
④ middleware를 여러개
middleware를 여러개 붙일 수도 있다. 여기서 next()를 호출하면 그 다음의 함수를 실행하라는 것과 마찬가지.
즉 순서대로 ↓ 진행되는 것이다.
④-1
app.use('/user/:id', function (req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}, function (req, res, next) {
console.log('Request Type:', req.method)
next()
})
④-2
app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id)
next()
}, function (req, res, next) {
res.send('User Info')
})
// handler for the /user/:id path, which prints the user ID
app.get('/user/:id', function (req, res, next) {
res.end(req.params.id)
})
둘다 app.get 메서드에 경로도 '/user/:id'로 같다.
순서대로 실행되기 때문에 제일 먼저
function (req, res, next) {
console.log('ID:', req.params.id)
가 실행되고
next()가 되면서
function (req, res, next) {
res.send('User Info')
})
가 실행되고 여기선 아무런 next함수 실행 여부가 적혀있지 않기 때문에 두번째 app.get은 실행되지 않고 여기서 끝난다.
반면
④-3
이 경우에는 조건문을 사용해서
app.get('/user/:id', function (req, res, next) {
// if the user ID is 0, skip to the next route
if (req.params.id === '0') next('route')
// otherwise pass the control to the next middleware function in this stack
else next()
}, function (req, res, next) {
// send a regular response
res.send('regular')
})
// handler for the /user/:id path, which sends a special response
app.get('/user/:id', function (req, res, next) {
res.send('special')
})
if (req.params.id === '0') next('route')
이 경우이면 next('route') 즉 다음 라우트의 미들웨어를 실행시키라는 의미이므로
// handler for the /user/:id path, which sends a special response
app.get('/user/:id', function (req, res, next) {
res.send('special')
})
이게 실행된다.
반면
else next()
이 경우이면 바로 다음 미들웨어를 실행시키라는 것이므로
function (req, res, next) {
// send a regular response
res.send('regular')
})
이게 실행된다.
6. 에러처리
http://expressjs.com/en/guide/error-handling.html
① 위에 있는 라우터 경로에 하나도 해당이 안되는 경우
router.use(function (req, res) {
res.status(404).send('Sorry cant find that!')
})
//+ 맨뒤에 에러처리 middleware을 붙여준다. 왜냐하면 middleware는
//순차적으로 처리하는데 여기까지 내려왔다는건 처리를 못하고 에러가 났다는 거니까
② 아니면 중간에 에러처리를 해주고 싶다면 조건문을 사용해서
if 에러가 있으면 next(err) 라고 next의 인자로 err을 준다. 그러면 express내에 있는 에러처리 미들웨어를 사용하게된다. .
router.get('/messages', cors(corsOptions), (req, res) => {
if(err){
next(err)
}
else {
res.send(results.results)
}
})
에러 발생시 나오는 문구 등 error handlers을 작성하고 싶다면, 코드 맨 끝에
아래와 같은 형식으로 적어준다.
Node.js는 function(err, req, res, next) 처럼 인자가 4개가 등록되어있는 error-handling function으로 정의하기 때문이다.
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
7. CORS
https://github.com/expressjs/cors
여기에 매우 잘 정리되어 있다.
express에서는
- origin: Access-Control-Allow-Origin CORS 헤더를 구성.
- methods: Access-Control-Allow-Methods CORS 헤더를 구성. 쉼표로 구분 된 문자열 (예 : 'GET, PUT, POST') 또는 배열 (예 :)이 필요 ['GET', 'PUT', 'POST']한다.
- allowedHeaders: Access-Control-Allow-Headers CORS 헤더를 구성.
- credentials: Access-Control-Allow-Credentials CORS 헤더를 구성.
- maxAge: Access-Control-Max-Age CORS 헤더를 구성.
그리고 이들은 배열이나 쉼표로 구분된 문자열로 지정해주면 된다.
※ Express를 사용하지 않았을 때
const headers = defaultCorsHeaders;
const defaultCorsHeaders = {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-headers": "content-type, accept",
"access-control-max-age": 10 // Seconds.
};
※ Express를 사용 했을 때
const cors = require('cors');
server.use(cors);
var corsOptions = {
origin: '*' ,
methods: 'GET, PUT, POST',
allowedHeaders: 'Content-Type, Accept',
maxAge: 10
}
Reference
https://wikibook.co.kr/article/what-is-expressjs/
'Dev > SW Engineering' 카테고리의 다른 글
22. React - Intro & 12 main concepts (Getting start, JSX, Rendering Element) (0) | 2020.07.21 |
---|---|
21. Server & Node - CommonJS/RESTful API (0) | 2020.07.21 |
Server&Node Sprint - Node.js를 이용한 서버 구축(HTTP 트랜잭션 해부) (0) | 2020.07.21 |
19. Server & Node - Concepts (0) | 2020.07.21 |
16. Interaction With Server - Why We Use Fetch & Let's Use! (0) | 2020.07.21 |