section2/Unit10/[Web Server] 기초(10/14~17)
블로깅 주제
- [Web Server] 기초
1. 지금 현재, 당신의 기분이나 느낌을 표현해 주세요.
- 드디어 금요일이다.. 어제 보이는 라디오에 참여하며 여러가지 감정들을 느꼈지만, 가장 내 마음을 울린 한 문장을 적어본다.
'기적에도 시간이 필요하다'.
2. 오늘 무엇을 학습한 내용 중 지금 떠올릴 수 있는 단어를 모두 나열해 주세요.
- Express
3. 2에서 작성한 단어를 가지고, 오늘의 학습 내용을 설명해 보세요.
- Express
Express는 Node.js 환경에서 웹 서버, 또는 API 서버를 제작하기 위해 사용되는 인기 있는 프레임워크이다.
Express로 구현한 서버가 Node.js HTTP 모듈로 작성한 서버와 다른 점은 다음과 같다.
- 미들웨어를 추가할 수 있다.
- 라우터를 제공한다.
간단한 웹 서버 만들기
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 on port ${port}`)
})
라우팅: 메서드와 url에 따라 분기(Routing)하기
클라이언트의 요청에 해당하는 메서드와 url(/lower, /upper 등)로 분기점을 만드는 것을 라우팅(Routing)이라고 한다.
클라이언트는 특정한 HTTP 요청 메서드(GET, POST 등)와 함께 서버의 특정 URI(또는 경로)로 HTTP 요청을 보낸다. 라우팅은 클라이언트의 요청에 해당하는 Endpoint에 따라 서버가 응답하는 방법을 결정하는 것이다.
const router = express.Router()
router.get('/lower', (req, res) => {
res.send(data);
})
router.post('/lower', (req, res) => {
// do something
})
▶ 미들웨어
자동차 공장에서는 컨베이어 벨트 위에 올려진 자동차의 뼈대에, 각 공정마다 부품을 추가한다. 모든 부품이 추가되면 완성된 자동차가, 어딘가 문제가 있다면 불량품이 결과물로 나오게 된다.
미들웨어(Middleware)는 자동차 공장의 공정과 비슷하다. 컨베이어 벨트 위에 올라가 있는 요청(Request)에 필요한 기능을 더하거나, 문제가 발견된 불량품을 밖으로 걷어내는 역할을 한다. 미들웨어는 express의 가장 큰 장점이라고 할 수 있다.
미들웨어를 사용하는 상황은 다음과 같다.
- POST 요청 등에 포함된 body(payload)를 구조화할 때(쉽게 얻어내고자 할 때)
- 모든 요청/응답에 CORS 헤더를 붙여야 할 때
- 모든 요청에 대해 url이나 메서드를 확인할 때
- 요청 헤더에 사용자 인증 정보가 담겨있는지 확인할 때
미들웨어를 이용하면 Node.js 만으로 구현한 서버에서는 번거로울 수 있는 작업을 보다 쉽게 적용할 수 있다.
공식 문서에서 확인할 수 있는 미들웨어의 구성
- get
- 미들웨어 함수가 적용되는 HTTP method
- '/'
- 미들웨어 함수가 적용되는 경로(Route)
- function()
- 미들웨어 함수
- function안에 있는 (req, res, next)중에서 req
- 미들웨어 함수에 대한 HTTP 요청 인수(일반적으로 req)
- function안에 있는 (req, res, next)중에서 res
- 미들웨어 함수에 대한 HTTP 응답 인수(일반적으로 res)
- function안에 있는 (req, res, next)중에서 next
- 미들웨어 함수에 대한 콜백 인수(일반적으로 next). 다음 미들웨어를 실행.
case 1: POST 요청 등에 포함된 body(payload)를 구조화할 때
Node.js로 HTTP body(payload)를 받을 때에는 Buffer를 조합해서 다소 복잡한 방식으로 body를 얻을 수 있다. 네트워크 상의 chunk를 합치고, buffer를 문자열로 변환하는 작업이 필요하다.
let body = [];
request.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
// body 변수에는 문자열 형태로 payload가 담겨져 있습니다.
});
Express v4.16.0 부터는 body-parser를 따로 설치 하지 않고, Express 내장 미들웨어인 express.json()을 사용한다.
const jsonParser = express.json();
// 생략
app.post('/api/users', jsonParser, function (req, res) {
})
※ Mini-Node Server 리팩토링을 진행하다가 express.json() 미들웨어 사용에 에러가 난다면? express.json([options])의 options에 해답이 있다. options에 {strict: false}를 추가한다.
const jsonParser = express.json({strict: false});
// 생략
app.post('/api/users', jsonParser, function (req, res) {
})
기존에는 빈 배열이나 빈 문자열을 만든 후 데이터를 순차적으로 추가하여 문자열로 변경해주는 형태로 코드를 작성했지만 Express의 경우, express.json({strict: false})을 사용하면 req.body에 JSON 형태의 payload가 담기게 된다.
express.json은 여러 옵션이 있는데 그 중 strict의 기본값은 true로, 배열이나 객체 형태만 인식하므로 위 코드처럼 {strict: false}로 설정해주어야 한다.
case 2: 모든 요청/응답에 CORS 헤더를 붙일 때
Node.js HTTP 모듈을 이용한 코드에 CORS 헤더를 붙이려면, 응답 객체의 writeHead 메서드를 이용할 수 있다. Node.js에서는 이 메서드 등을 이용하여 라우팅마다 헤더를 매번 넣어주어야 한다. 그뿐만 아니라, OPTIONS 메서드에 대한 라우팅도 따로 구현해야 한다.
const defaultCorsHeader = {
'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
};
// 생략
if (req.method === 'OPTIONS') {
res.writeHead(201, defaultCorsHeader);
res.end()
}
cors 미들웨어를 사용하면 이 과정을 간단하게 처리할 수 있다.
npm install cors
1. 모든 요청/응답에 CORS헤더를 붙일 때
const cors = require('cors')
app.use(cors())
2. 특정 메소드에만 붙일 때
const cors = require('cors')
app.get('/products/:id', cors(), function (req, res, next) {
res.json({msg: 'This is CORS-enabled for a Single Route'})
})
case 3: 모든 요청에 대해 url이나 메서드를 확인할 때
미들웨어는 말 그대로 프로세스 중간에 관여하여 특정 역할을 수행한다. 수많은 미들웨어가 있지만, 가장 단순한 미들웨어 로거(logger)를 예로 들어보자. 로거는 디버깅이나, 서버 관리에 도움이 되기 위해 console.log로 적절한 데이터나 정보를 출력한다. 데이터가 여러 미들웨어를 거치는 동안 응답할 결과를 만들어야 한다면, 미들웨어 사이사이에 로거를 삽입하여 현재 데이터를 확인하거나, 디버깅에 사용할 수 있다.
만약 특정 enpoint가 아니라, 모든 요청에 동일한 미들웨어를 적용하려면 어떻게 해야 할까? 메서드 app.use 를 사용하면 된다. 아래 코드를 실행해보면 모든 요청에 대해 LOGGED가 출력되는 걸 확인할 수 있다.
const express = require('express');
const app = express();
const myLogger = function (req, res, next) {
console.log('LOGGED');
next();
};
app.use(myLogger);
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(3000);
모든 요청에 대해 메서드와 url을 출력하려면?
const express = require('express');
const app = express();
const port = 3000;
const cors = require('cors');
let myLogger = (req, res, next) => {
console.log(`http request method is ${req.method}, url is ${req.url}`);
next();
};
app.use(myLogger);
app.use(cors());
app.get('/', (req, res) => {
res.status(200).send('hello world');
});
app.listen(port, () => {
console.log(`this app listening at http://localhost${port}`);
});
case 4: 요청 헤더에 사용자 인증 정보가 담겨있는지 확인할 때
다음은 HTTP 요청에서 토큰이 있는지를 판단하여, 이미 로그인한 사용자일 경우 성공, 아닐 경우 에러를 보내는 미들웨어 예제이다.
토큰(Token): 주로 사용자 인증에 사용하며, Section3 인증(Authentication) 유닛에서 다룹니다.
app.use((req, res, next) => {
// 토큰이 있는지 확인, 없으면 받아줄 수 없음.
if(req.headers.token){
req.isLoggedIn = true;
next();
} else {
res.status(400).send('invalid user')
}
})
로그인 없이 웹사이트에 접근을 시도했을 때, 로그인 창으로 되돌려 보내는 경우를 경험해 본 적이 있을 것이다. 서버에서는 요청에 포함된 데이터를 통해 미들웨어가 요구하는 조건에 맞지 않으면, 불량품으로 판단하고 돌려보내도록 구현할 수 있다.
- 과제 : Refractor Express
basic-server.js
// Express ver
const express = require('express');
const app = express();
const port = 4999;
const jsonParser = express.json({strict:false});
const cors = require('cors');
app.use(jsonParser);
app.use(cors());
const myLogger = function(req,res,next){
console.log(req.method, req.url);
console.log(req.body);
next();
}
app.use(myLogger);
app.get('/',(req,res) => {
console.log(req.method,req.url);
res.send('Hello world');
})
app.post('/lower', (req,res) => {
res.json(req.body.toLowerCase()); // 다시 데이터를 보내줄 때 json화 해서 보내주는 메소드
})
app.post('/upper', (req,res) => {
res.json(req.body.toUpperCase());
})
app.use(function(req, res, next) { // 400에러 즉, 클라이언트의 잘못으로 인한 에러는 인자에 err를 넣지 않는다.
res.status(404).send('Something broke! 404');
});
app.use(function(err, req, res, next) { //에러 처리
console.error(err.stack);
res.status(500).send('Something broke! 500');
});
app.listen(port, ()=>{
console.log(`Example app listening on port ${port}`);
})
- 과제 : StatesAirline 서버
※ req.query/params/body 비교
1. rest.query : 주로 GET 요청에 대한 처리
// 요청온 url : www.example.com/public/100/jun?title=hello!
app.use(express.urlencoded({ extended: false })); // uri 방식 폼 요청 들어오면 파싱
router.get('/:id/:name', (req, res, next) => {
//../100/jun 부분이 담기게 된다.
console.log(req.params) // { id: '100', name: 'jun' }
// title=hello! 부분이 담기게 된다.
console.log(req.query) // { title : 'hello!' }
});
2. req.params : 라우터의 매개변수. 경로 관련
// 요청온 url : www.example.com/public/100/jun
router.get('/:id/:name', (req, res, next) => {
//../100/jun 부분이 담기게 된다.
console.log(req.params) // { id: '100', name: 'jun' }
});
3. req.body : POST에서 주로 쓰임. 유저 정보. JSON 등의 바디 데이터를 담을때 사용.
클라이언트
// 클라이언트 단에서, 자바스크립트로 Post요청을 보냄
// 방식 1
await axios.post('www.example.com/post/1/jun', {
name: 'nomad', // post 로 보낼 데이터
age: 11,
married: true
});
// 방식 2
await axios({
method: "post",
url: `www.example.com/post/1/jun`,
data: { // post 로 보낼 데이터
name: 'nomad',
age: 11,
married: true
},
})
서버
app.use(express.json()); // json 형식 폼 요청 들어오면 파싱
// 요청온 url : www.example.com/public/100/jun
router.post('/:id/:name', (req, res, next) => {
// public/100/jun 부분이 담기게 된다.
console.log(req.params) // { id: '100', name: 'jun' }
// post보낼때 담은 객체 부분이 담기게 된다.
console.log(req.body) // { name: 'nomad', age: 11, married: true }
});
[EXPRESS] 📚 req.params / req.query / req.body 🤔 차이 정리
req.params - 라우터 매개변수 예를 들어 /:id/:name 경로가 있으면 ":id"속성과 ":name"속성을 req.params.id, req.params.name으로 사용할 수 있다. www.example.com/post/1/jun 일 경우 1과 jun을 받는다. //..
inpa.tistory.com
flightController.js
const flights = require('../repository/flightList');
const fs = require('fs');
module.exports = {
// [GET] /flight
// 요청 된 파라미터 departure_times, arrival_times 값과 동일한 값을 가진 항공편 데이터를 조회합니다.
// 요청 된 파라미터 departure, destination 값과 동일한 값을 가진 항공편 데이터를 조회합니다.
findAll: (req, res) => {
const { departure_times, arrival_times, destination, departure } = req.query;
// TODO:
let filteredFlights = flights;
if(req.query.departure_times && req.query.arrival_times !== undefined){
console.log(req.query.departure_times,req.query.arrival_times);
filteredFlights = flights.filter ((flights) => {
return flights.departure_times === req.query.departure_times && flights.arrival_times === req.query.arrival_times;
})
}
if(req.query.destination && req.query.departure !== undefined){
console.log(req.query.destination,req.query.departure);
filteredFlights = flights.filter ((flights) => {
return flights.destination === req.query.destination && flights.departure === req.query.departure;
})
}
return res.status(200).json(filteredFlights);
},
// [GET] /flight/:uuid
// 요청 된 uuid 값과 동일한 uuid 값을 가진 항공편 데이터를 조회합니다.
findById: (req, res) => {
const { uuid } = req.params;
// TODO:
let filteredFlights = flights.filter((flights) => {
return flights.uuid === uuid;
})
return res.status(200).json(filteredFlights);
},
// Advanced
// [PUT] /flight/:uuid 요청을 수행합니다.
// 요청 된 uuid 값과 동일한 uuid 값을 가진 항공편 데이터를 요쳥 된 Body 데이터로 수정합니다.
update: (req, res) => {
const { uuid } = req.params;
const bodyData = req.body;
// TODO:
let index = 0;
flights.forEach((el, id) => {
if(el.uuid === uuid) {
index = id;
}
})
// console.log(index);
let filteredFlights = flights.filter((flights) => {
return flights.uuid === uuid;
})
flights[index] = {...filteredFlights[0],...bodyData};
return res.status(200).json(flights[index]);
}
};
bookController.js
// POST /book에서 사용할 uuid입니다.
const { v4: uuid } = require('uuid');
// 항공편 예약 데이터를 저장합니다.
let booking = [];
module.exports = {
// [GET] /book 요청을 수행합니다.
// 전체 예약 데이터를 조회합니다.
findAll: (req, res) => {
return res.status(200).json(booking);
},
// [GET] /book/:phone 요청을 수행합니다.
// 요청 된 phone과 동일한 phone 예약 데이터를 조회합니다.
findByPhone: (req, res) => {
const {phone} = req.params;
let filteredBook = booking.filter((book)=>{
return book.phone === phone;
})
return res.status(200).json(filteredBook);
},
// [GET] /book/:phone/:flight_uuid 요청을 수행합니다.
// 요청 된 id, phone과 동일한 uuid, phone 예약 데이터를 조회합니다.
findByPhoneAndFlightId: (req,res) => {
const {phone, flight_uuid} = req.params;
// TODO:
let filteredBook = booking.filter((book)=>{
return book.phone === phone && book.flight_uuid === flight_uuid;
})
return res.status(200).json(filteredBook);
},
// [POST] /book 요청을 수행합니다.
// 요청 된 예약 데이터를 저장합니다.
create: (req, res) => {
// POST /book에서 사용할 booking_uuid입니다.
const booking_uuid = uuid();
// TODO:
console.log(req.body);
let book = {
"booking_uuid" : booking_uuid,
"flight_uuid" : req.body.flight_uuid,
"name" : req.body.name,
"phone" : req.body.phone
}
booking.push(book);
return res.status(200).json(booking);
},
// Optional
// [DELETE] /book/:booking_uuid 요청을 수행합니다.
// 요청 된 id, phone 값과 동일한 예약 데이터를 삭제합니다.
deleteByBookingId: (req, res) => {
const {booking_uuid} = req.params;
// TODO:
}
};