* 이전 글 : StatesAirline Server API document
* Airline Server 개발하기
- Airport API
- Book API
- Flight API
초기세팅
폴더 구조
1. statesairline
- statesairline/app.js 는 서비스에 필요한 미들웨어와 웹 서버를 실행하는 코드가 작성되어 있다.
2. router
- statesairline/router/ 안에는 airport API, book API, flight API 요청을 수행하는 라우터가 작성되어 있다. 작성된 라우터 내용을 통해 API 요청을 받을 수 있다.
3. controller
- statesairline/controller/ 안에는 정의된 API 요청을 수행하는 코드가 작성되어 있다.
4. repository
- statesairline/repository/flightlist.js 는 서비스에서 제공하는 항공편 데이터가 작성되어 있다. (이 데이터로 서비스를 구현)
- statesairline/repository/airportlist.js 는 서비스에서 제공하는 공항 데이터가 작성되어 있다.
5. test
- statesairline/__test__/statesairline.test.js 은 Jest 스펙을 가지고 있으며 코드 테스트를 위한 테스트 케이스가 작성되어 있다.
초기 세팅
더보기
- npm install 을 통해서 package.json에 설정된 패키지를 설치한다.
- npm start를 통해 서버를 실행한다.
- Postman으로 API 동작 여부를 확인한다.
- npm test를 통해 테스트를 실행한다.
- 서버에서 사용되는 포트 번호로 실행되지 않는 경우, 관리자 권한을 부여하여 실행하거나 해당 포트를 사용 중인 프로그램을 종료 후 재시작을 한다.
미리 알아야 할 것
req.query와 req.params
- Query와 Params를 기준으로 데이터를 필터링한다.
1. statesairline
app.js
// 서비스에 필요한 미들웨어와 웹 서버를 실행하는 코드
const express = require('express');
const cors = require('cors');
const app = express();
// 모든 서버는 요청을 받을수 있는 포트 번호를 필요로 한다.
// HTTP server의 표준 포트는 보통 80 번 이지만, 보통 다른 서버에서 사용중이기 때문에 접근할 수 없다.
// 따라서 우리는 테스트 서버 포트로 3000, 8080, 1337 등을 활용한다.
// PORT는 아파트의 호수와도 같다. 서버로 요청을 받기 위해서는 다음과 같이 포트 번호를 설정 한다.
// (* 때에 따라 다른 포트번호를 열고 싶다면, 환경 변수를 활용 하기도 한다.)
// 환경변수 : 프로세스가 컴퓨터에서 동작하는 방식에 영향을 미치는 동적인 값들의 모임
const port = 3001;
const flightRouter = require('./router/flightRouter'); // router require
const bookRouter = require('./router/bookRouter');
const airportRouter = require('./router/airportRouter');
// cors 헤더를 모든 응답에 넣어준다.
app.use(cors());
// 모든 응답은 json으로 한다. Content-Type: application/json
app.use(express.json());
app.use('/flight', flightRouter); // 비행 일정 조회는 /flight
app.use('/book', bookRouter); // 항공권 예약는 /book
app.use('/airport', airportRouter); // 공항 정보 조회는 /airport
app.get('/', (req, res) => {
res.status(200).send('Welcome, States Airline!');
});
app.use((req, res, next) => { // 🟣 (설명1)
res.status(404).send('Not Found!');
});
app.use((err, req, res, next) => { // 🟣 (설명2)
console.error(err.stack);
res.status(500).send({
message: 'Internal Server Error',
stacktrace: err.toString() // 🟣 (설명2)
});
});
app.listen(port, () => {
console.log(`[RUN] StatesAirline Server... | http://localhost:${port}`);
});
module.exports = app;
🟣 (설명 1), (설명 2)
Express.js 프레임워크에서 에러 핸들링과 예외 처리를 위한 미들웨어를 등록하는 방법
(설명 1) 첫 번째 미들웨어 함수는 404 Not Found 오류를 처리한다. 이 미들웨어는 모든 요청에 대해 호출되며, 라우터 로직에서 해당 요청에 대한 핸들러를 찾지 못한 경우 실행된다. 이 경우, 클라이언트에게 "Not found!" 메시지와 함께 404 코드를 반환한다.
(설명 2) 두 번째 미들웨어 함수는 500 Internal Server Error 오류를 처리한다. 이 미들웨어는 다른 미들웨어나 라우터 로직에서 예외가 발생했을 때 실행된다. 이 경우 서버 내부 오류를 나타내는 500 상태 코드를 반환하고, 서버 로그에 에러 스택 트레이스를 기록한다.
-> 클라이언트에게 적절한 오류 메시지와 함께 적절한 HTTP 코드를 반환하여 웹 애플리케이션의 안정성과 신뢰성을 높이는 역할
🟣 (설명 3) : 500 Internal Server Error
하이퍼텍스트 전송 프로토콜 (HTTP) 500 Internal Server Error 서버 에러 응답 코드는 요청을 처리하는 과정에서 서버가 예상하지 못한 상황에 놓였다는 것을 나타낸다.
- HTTP 상태 코드 500은 다른 오류 코드가 적합하지 않을 때 서버에서 반환되는 일반적인 오류 응답이다.
- 서버 언어의 구문 에러(스크립트 문법 오류), 서버 통신의 Timeout 시간 지연 오류, 서버 트래픽 과부하 등의 이유가 있다.
- 아래의 경우, POST 요청의 body 부분의 SyntaxError가 이유이다. ( 4라인 마지막에 , 를 지워야함)
stacktrace: err일 때,
stacktrace: err.toString()일 때,
2. router
1) airport router & airport controller
airport router
const { findAll } = require('../controller/airportController');
const express = require('express');
const router = express.Router(); // **
router.get('/', findAll); // airportController의 findAll 기능을 / 주소로 연결
module.exports = router; // **
airport controller
const airports = require('../repository/airportList'); // 공항 정보 데이터
module.exports = {
// [GET] /airport?query={query} 요청을 수행
// 공항 이름 자동완성 기능을 수행
findAll: (req, res) => {
if (req.query.query) { // 🟣 (설명1)
// console.log(req.query.query);
const filteredAirports = airports.filter((airport) => {
return airport.code.includes(req.query.query.toUpperCase());
});
return res.status(200).json(filteredAirports);
}
res.json(airports);
}
};
🟣 (설명 1) req.query.query?
http://localhost:3001/airport?query=a 일 때
1) req
2) req.query와 req.query.query
4) postman에서의 res
airportList (데이터... 중간 생략)
module.exports = [
{
name: '제주',
code: 'CJU'
},
.
.
(생략)
.
];
2) book router & book controller
bookRouter.js
const { findAll, findByPhone, findByPhoneAndFlightId, create, deleteByBookingId } = require('../controller/bookController');
const express = require('express');
const router = express.Router();
router.get('/', findAll);
router.get('/:phone', findByPhone);
router.get('/:phone/:flight_uuid', findByPhoneAndFlightId);
router.post('/', create);
router.delete('/:booking_uuid', deleteByBookingId);
module.exports = router;
bookController.js
// POST /book에서 사용할 uuid
const { v4: uuid, validate } = require('uuid');
// 항공편 예약 데이터를 저장
let booking = [];
const isKoreanPhoneNumber = (str) => {
return /^01([0|1|6|7|8|9]?)-?([0-9]{3,4})-?([0-9]{4})$/.test(str);
};
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;
if (!isKoreanPhoneNumber(phone)) return res.status(400).json('Bad request');
const filteredPhone = booking.filter((info)=> info.phone === phone )
if (filteredPhone.length === 0) return res.status(404).json('Not found');
return res.status(200).json(filteredPhone)
},
// 🟡 [GET] /book/:phone/:flight_uuid 요청
// 요청 된 id, phone과 동일한 uuid, phone 예약 데이터를 조회
findByPhoneAndFlightId: (req,res) => {
const {phone, flight_uuid} = req.params;
if (!isKoreanPhoneNumber(phone) || !validate(flight_uuid)) return res.status(400).json("Bad request")
const filteredPhoneAndId = booking.filter((info)=>
info.phone === phone && info.flight_uuid === flight_uuid );
if (filteredPhoneAndId.length === 0) return res.status(404).json('Not found');
return res.status(200).json(filteredPhoneAndId)
},
// 🟡 [POST] /book 요청
// 요청 된 예약 데이터를 저장
create: (req, res) => {
const booking_uuid = uuid(); // POST /book에서 사용할 booking_uuid
const {phone, flight_uuid, name} = req.body;
if (!isKoreanPhoneNumber(phone)) return res.status(400).json('Bad request');
if (booking.find((book) => book.phone === phone && book.flight_uuid === flight_uuid)) {
return res.status(409).json("It's Already booked.");
} else {
booking.unshift({ booking_uuid, flight_uuid, name, phone });
res.location(`/book/${booking_uuid}`); // 🟣 (설명 1)
return res.status(201).json(booking[0]);
}
},
// 🟡 [DELETE] /book/:booking_uuid 요청을 수행
// 요청 된 id, phone 값과 동일한 예약 데이터를 삭제
deleteByBookingId: (req, res) => {
const {booking_uuid} = req.params;
booking = booking.filter((item) => item.booking__uuid !== booking_uuid);
return res.status(200).json(booking_uuid) // 🟣 (설명 2)
}
};
🟣 (설명 1) res.location(`/book/${booking_uuid}`);
🟣 (설명 2) return res.status(200).json(booking_uuid);
- 204 No Content : 삭제 요청으로 자원을 삭제하여 더 이상 존재하지 않고 그 자원을 참조하는 모든 자원도 삭제되어 더 이상 HTTP body를 응답하는 것이 무의미해졌을 때 사용한다.
- 즉, express에서 200과 body에 null, false 등으로 응답하는 것과 달리 204는 HTTP Response body가 아예 존재하지 않는다.
- 그러나 API 문서에 미리 작성해 놓은 것처럼 응답 메시지에 삭제가 완료된 booking_uuid를 포함하였다.
- 아래는 GitHub Pages site를 삭제할 때의 REST API 예시이다.
3) flight router & flight controller
flightRouter.js
const { findAll, findById, update } = require('../controller/flightController');
const express = require('express');
const router = express.Router();
router.get('/', findAll);
router.get('/:uuid', findById);
router.put('/:uuid', update);
module.exports = router;
flightController.js
(1) findAll - 첫 번째 방법
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;
if(departure_time && arrival_times) {
const filteredTime = flights.filter((item) => {
return item.departure_times === departure_times &&
item.arrival_times === arrival_times} )
return res.status(200).json(filteredTime);
}
if(destination && departure) {
const filteredPlace = flights.filter((item) => {
return item.destination === destination &&
item.departure === departure} )
return res.status(200).json(filteredPlace);
}
res.status(200).json(flights);
},
(2) findAll - 두 번째 방법
findAll: (req, res) => {
const {
departure_times, arrival_times,
destination, departure} = req.query;
// 유효한 입력인지를 먼저 확인한다.
const isValidAirportCode = (code) => {
// Check if the code is a string of three uppercase letters
return (
typeof code === "string" && code.length === 3 && /^[A-Z]{3}$/.test(code)
);
};
const isValidDate = (timestamp) => { // 🟣 (설명1)
const date = new Date(timestamp);
return !isNaN(date.getTime()) && date.toISOString().split('T')[0] === timestamp.split('T')[0];
};
if (Object.keys(req.query).length === 0) return res.json(flights);
else if (isValidDate(departure_times) && isValidDate(arrival_times)) {
const data = flights.filter(
(flight) =>
flight.departure_times === departure_times && // 🟣 (설명2) 출발시간만 있을 경우 추가
flight.arrival_times === arrival_times
);
return res.status(200).json(data);
} else if (isValidAirportCode(departure) && isValidAirportCode(destination)) {
const data = flights.filter(
(flight) =>
flight.departure === departure && flight.destination === destination
);
return res.status(200).json(data);
} else {
return res.status(400).json("Incorrect request");
}
},
🟣 (설명 1) isValidDate() 함수
- isValidDate() 함수는 입력받은 timestamp를 Date 객체로 변환한 다음, 날짜 형식이 유효한지 확인한다. 따라서 입력받은 timestamp가 Date 객체로 정확하게 변환될 수 있는 형식이어야 한다.
- 입력한 departure_times 값을 보면 '2023-04-09T10:00:00.000Z'로, ISO 8601 형식을 따르고 있다. 이 형식은 유효한 날짜 및 시간 값을 나타내기 위한 국제 표준 형식 중 하나이며, 대부분의 프로그래밍 언어에서 지원한다. 그러므로 이 형식에 맞춰서 값을 입력하면 isValidDate() 함수에서는 ture를 반환할 것이다.
- 다만, 입력한 값이 직접 생성한 문자열이라면, 올바른 날짜 값을 가지고 있지 않을 가능성이 있다. 이 경우, 입력된 값을 직접 확인하거나, 라이브러리를 이용하여 날짜를 생성하는 것이 좋다. 예를 들어, moment.js와 같은 라이브러리를 이용하면, 다양한 날짜 형식을 쉽게 다룰 수 있다.
🟣 (설명 2) 출발 시간만 입력할 경우
-> 실제 비슷한 코드를 짜게 된다면 조금 더 나은 사용자 경험을 위해서, 비행기 출발 시간만 입력해도 도착시간이 다양한 비행기편이 나오도록 하는 것이 좋을 것 같다.
else if (isValidDate(departure_times)) { if(isValidDate(arrival_times) && arrival_times) const data = flights.filter((flight) => flight.departure_times === departure_times && flight.arrival_times === arrival_times); return res.status(200).json(data); } else { const data = flights.filter((flight) => flight.departure_times === departure_times); return res.status(200).json(data); }
// 🟡 [GET] /flight/:uuid
// 요청 된 uuid 값과 동일한 uuid 값을 가진 항공편 데이터를 조회
findById: (req, res) => {
const { uuid } = req.params;
if( uuid ) {
const filteredID = flights.filter((item) => item.uuid === uuid) // 데이터가 없으면 []
// const filteredID = flights.find((item) => item.uuid === uuid) 이렇게도 쓸 수 있다. 데이터가 없으면 undefined
return res.status(200).json(filteredID);
}
},
// 🟡 [PUT] /flight/:uuid 요청을 수행
// 요청 된 uuid 값과 동일한 uuid 값을 가진 항공편 데이터를 요쳥된 Body 데이터로 수정
update: (req, res) => {
const { uuid } = req.params; // 비행기 ID
const bodyData = req.body; // 업데이트 희망하는 객체
if( uuid ) {
flightFiltered = flights.filter((flight) => flight.uuid === uuid) // 해당하는 비행편이 나왔음
let updateList = {}
// 🟣 (1) 필요하면 어느 부분이 업데이트 되었는지도 알려주기 위한 코드
for(let key in bodyData) {
if(bodyData[key] !== flightFiltered[0][key]) { // 요청 중 기존 데이터와 다른 부분이 있다면
updateList[key] = bodyData[key] // 업데이트에 추가
flightFiltered[0][key] = bodyData[key] // 갱신된 정보로 덮어쓴다.
}
}
// 🟣 (2) 업데이트가 된 부분을 알려줄 필요가 없다면 아래의 코드로도 충분하다.
// flightFiltered[0] = {...flightFiltered[0], ...bodyData} 또는 Object.assign(flightFiltered[0], bodyData)
return res.status(202).json(flightFiltered[0]);
// 🟣 (3) 또는 이렇게도 가능하다.
// const beUpdatedIdx = flights.findIndex((flight) => flight.uuid === uuid);
// const updatedFlight = { ...flights[beUpdatedIdx], ...bodyData }; //최종적으로 업데이트된 flight
// flights.splice(beUpdatedIdx, 1, updatedFlight); // 덮어쓰기
// return res.status(200).json(updatedFlight);
}
}
};
728x90
'FE > Server' 카테고리의 다른 글
쿠키(Cookie)의 개념과 작동원리 (0) | 2023.05.02 |
---|---|
mkcert를 이용한 로컬에서 HTTPS 서버 만들기 (0) | 2023.04.28 |
[Network] Airline 서버 구현 (1) : Airline Server API document (0) | 2023.04.06 |
[Network] Express로 간단한 웹 서버 만들어보기 (라우터) (0) | 2023.04.05 |
[Network] HTTP 모듈을 사용한 서버 다뤄보기 (0) | 2023.04.04 |