39일차 StatesAirline Server

2021. 9. 11. 17:57
반응형

2021. 09. 10 금요일

1. Today's Key Points!🔑

  • CORS
  • Routing
  • Express
  • middleware

2. 정리해보자!🧹

3일 동안 웹 서버 구축을 하는 기초단계를 학습해왔다. mini-node-server를 순수 node.js로 구현해보았고, express와 미들웨어를 사용해서 리팩토링도 해보았다.

👉🏻오늘은 CORS에 대해서 정리해보고자 한다.

CORS가 무엇인가? Cross Origin Resource Sharing의 약자로, 시스템 수준에서 타 도메인 간 자원 호출을 승인하거나 차단하는 것을 결정하는 것이다. 예를들어, 나 너네 서버에 POST 요청해도 되니? 라는 요청을 먼저 보내고 해도 된다는 응답이 오면 그때 POST 요청을 보내는 것이다.

그럼 CORS가 왜 필요한 것인가? 다른 도메인에서의 자원을 호출하는 행위에 제한이 없으면 안전하지 않다. 그래서 서버가 허용한 클라이언트의 요청에만 응답하기 위해서 필요한 것이다.

서버가 CORS를 허용하기 위한 HTTP 헤더 각각의 목적을 이해해보자.

  • Access-Control-Allow-Origin : 단일 출처를 지정해서 브라우저가 해당 출처가 리소스에 접근하도록 허용한다. "*" 처리를 해주면 origin에 상관없이 모든 리소스에 접근하도록 허용해준다.
  • Access-Control-Allow-Methods : 리소스에 접근할 때 허용되는 메서드를 지정한다.
  • Access-Control-Allow-Headers : 사용자 정의 헤더를 받을 수 있음을 나타내준다. 예를 들어, " 'Access-Control-Allow-Headers' : 'X-PINGOTHER, Content-Type' " 이렇게 작성이 되어있다면, 'X-PINGOTHER, Content-Type' HTTP 헤더를 사용할 수 있게 된다.
  • Access-Control-Max-Age : prefilght request 요청 결과를 캐시할 수 있는 시간을 나타낸다. 다시 말해서 preflight request 유효기간을 설정해주는 것이다. 예를 들어 'Access-Control-Max-Age : 86400' 이라고 되어있으면, preflight request를 한번 보내고 허가를 받으면 86400초(24시간) 동안 preflight request를 굳이 보내지 않아도 리소스 요청을 할 수 있다.

다른 HTTP 헤더도 있는데, 그것에 대해서 알고 싶다면 공식문서에서 확인하면 된다.

3. Sprint 복기!🧐

States Airline 서버를 구현하는 과제이다. express 프레임워크를 이용해서 만든 서버이고, 클라이언트 요청에 따라 항공편과 예약 데이터를 조회, 생성, 수정, 삭제하는 기능을 수행할 수 있어야 한다.

우선 폴더의 구조를 이해하고 router, controller가 어떤 역할을 수행하는지 관찰해보자.

먼저 app.js를 살펴보면, express 프레임워크를 사용해서 서버를 구현했고, app.use를 통해 각각 flightRouter, bookRouter, airportRouter에 분기점을 만들어 주었다.

const express = require('express');
const cors = require('cors');
const app = express();

const port = 81;

const flightRouter = require('./router/flightRouter');
const bookRouter = require('./router/bookRouter');
const airportRouter = require('./router/airportRouter');

app.use(cors());
app.use(express.json());

app.use('/flight', flightRouter);
app.use('/book', bookRouter);
app.use('/airport', airportRouter);

app.get('/', (req, res) => {
  res.status(200).send('Welcome, States Airline!');
});

app.use((req, res, next) => {
  res.status(404).send('Not Found!');
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send({
    message: 'Internal Server Error',
    stacktrace: err.toString()
  });
});

app.listen(port, () => {
  console.log(`[RUN] StatesAirline Server... | http://localhost:${port}`);
});

module.exports = app;

각 Router.js 파일을 보면, 아래와 같이 작성되어 있다. 각 Router.js 파일에서는 Controller에서 작성된 함수들을 가져와서 각 요청에 맞는 함수를 실행시켜주고 있다.

//airportRouter.js
const { findAll } = require('../controller/airportController');
const exress = require('express');
const router = exress.Router();

router.get('/', findAll);

module.exports = router;

//flightRouter.js
const { findAll, findById, update } = require('../controller/flightController');
const express = require('express');

const router = express.Router();

router.get('/', findAll);

router.get('/:id', findById);

router.put('/:id', update);

module.exports = router;

//bookRouter.js
const { findById, create, deleteById } = require('../controller/bookController');
const exress = require('express');
const router = exress.Router();

router.get('/', findById);

router.post('/', create);

router.delete('/', deleteById);

module.exports = router;

결국 Controller 함수들이 Router.js에 불려와서 실행이되고, 이 라우터 들이 app.js에 불려와서 사용된다는 것을 알 수 있다. 그래서 app.js에 있는 
app.use('/flight', flightRouter); 는
app.get('/flight', findAll), app.get('/flight/:id', findById), app.put('/flight/:id', update)을 포함하고 있다고 볼수 있고,
app.use('/book', bookRouter); 는
app.get('/book', findById), app.post('/book', create), app.delete('/book', deleteById)을 포함하고 있다고 볼수 있고,
app.use('/airport', airportRouter); 는
app.get('/airport', findAll)을 포함하고 있다고 볼수 있다.

이렇게 폴더의 구조를 파악해 보았고, 이제 내가 해줘야 하는 것은 각 Controller.js에서 조회, 생성, 수정, 삭제 기능을 구현할 함수를 만들어주면 된다. 그럼 기능을 구현하러 가보자.

👉🏻flightController

여기서 구현해야할 기능은 [GET] /flight, [GET] /flight/{:id}, [PUT] /flight/{:id} 이다. 그리고 기능구현에 필요한 findAll, findById, update 함수를 만들어야 한다.

그럼 우선 findAll 함수부터 살펴보자.

클라이언트가 /flight?departure_times={departure_times}를 입력하면, 해당하는 departure_times의 데이터를 모두 보여주어야 한다. 또한, /flight?departure_times={departure_times}&arrival_times={arrival_times} 이렇게 입력하면, 입력된 정보에 해당하는 것만 보여주어야 한다.

그럼 우리가 해당 데이터만 보여주기 위해서는 클라이언트가 입력한 정보와, 우리가 가지고 있는 데이터와 비교를 해야한다.

클라이언트가 입력한 정보는 req.query 객체에 담기게 되고, 그럼 이 req.query 객체에있는 데이터와 flightList에 있는 데이터와 비교하면 된다.

클라이언트가 입력한 정보가 req.query 객체에 어떻게 담기냐 하면, 클라이언트가 /flight?departure=ICN&destination=CJU 를 입력하면 req.query = { departure : 'ICN', destination : 'CJU'}가 된다.

그래서 이것을 활용해서 flightList에 있는 데이터를 필터하면, 클라이언트가 원하는 정보만 볼 수 있게 된다.

그래서 다음과 같이 코드를 짜보았다. list라는 변수에 flightList 배열을 할당하고, 그것을 필터링 하는 코드이다. 출발지하나만 입력되는 것이 아니고 출발지, 목적지, 출발시간 이렇게 한번에 여러가지가 입력될 수 있으므로 저 세가지 모두가 필터된 정보를 우리는 보여줘야 한다. 그래서 list에 필터된 list를 할당하고 또 필터된 list에 필터된 list를 할당 하는 코드를 구현한 것이다.

const flights = require('../repository/flightList');

module.exports = {
  findAll: (req, res) => {
    if (req.query) { //쿼리가 입력되면
      let list = flights; //list라는 변수에 flightList 데이터를 할당하고
      if(req.query.destination){ //입력된 쿼리가 destination이면
        list = list.filter((item) => { //flightList 배열안에 있는 각 객체의 destination값을 모두 조회해서 입력된 destination과 같은 것만 필터해서 list에 담는다.
          return req.query.destination === item.destination;
        });
      }
      if(req.query.departure){ //입력된 쿼리가 departure이면
        list = list.filter((item) => {
          return req.query.departure === item.departure;
        });
      }
      if(req.query.departure_times){
        list = list.filter((item) => {
          return req.query.departure_times === item.departure_times;
        });
      }
      if(req.query.arrival_times){
        list = list.filter((item) => {
          return req.query.arrival_times === item.arrival_times;
        });
      }
      return res.status(200).json(list); //필터가 완료된 list를 json화 해서 보내준다.
    }
  }
  //생략

위의 코드를 보면 조건문 4가지를 사용해서 똑같은 일이 반복된다는 것을 볼 수 있을 것이다. 어떤 것과 어떤것이 비교되야 하는지가 조금 익숙해지면 저것을 반복을 통해 좀 더 간결하게 구현할 수 있을 것이다. 아래는 위의 코드를 좀 더 간결하게 구현한 것이다.

const flights = require('../repository/flightList');

module.exports = {
  findAll: (req, res) => {
    if(req.query){ //쿼리에 입력값이 있을 때
      let query = Object.keys(req.query); // query라는 변수에 req.query의 키값들만 배열에 담아준다.
      const list = flights.filter((item) => { // 그리고 list라는 변수에 flightList의 필터링 된 데이터를 담아준다.
        let isEqual = true; 
        query.forEach((el) => { //query 배열에 담긴 key를 순회해서
          if(req.query[el] !== item[el]){ //쿼리에 입력된것과 일치하지 않는 것만 빼고 list에 담아준다.
            isEqual = false;
          }
        })
        return isEqual;
      })
      return res.status(200).json(list);
    }
  }
  //생략

처음에 forEach 메소드에 대해 익숙하지 않아서 아래처럼 작성해주면 되는 거 아닌가 했다.

findAll: (req, res) => {
    if(req.query){
      let query = Object.keys(req.query);
      const list = flights.filter((item) => {
        return query.forEach((el) => {
          req.query[el] === item[el]
        })
      })
      return res.status(200).json(list);
    }
    //생략

순회하면서 true, true, false, false 이런식으로 나오면 앞에 true가 나온 요소만 배열에 담아줘야 하는 거 아니야? 하면서 계속 고민했다. 근데 forEach라는 메소드가 배열을 순회해서 각 배열에 함수를 실행시켜주는 메소드이다. 그렇기 때문에 forEach안에 저렇게 비교만 해놓으면 리턴되는 값이 없이 그냥 forEach안에서 값 비교만 하고 리턴되는 값은 없을 것이다. 그래서 리턴해줄 변수를 하나 만들어서 콜백 함수를 돌려 리턴되는 값이 나오도록 만들어 주어야 한다.

이렇게 findAll 함수를 구현했다. findAll 함수를 제대로 구현할 줄 알면 findById 함수도 똑같다. 다만 조금 다른게 있다면, 위는 req.query를 조회해야하고, findById는 req.params를 조회해야 한다는 것이다. req를 콘솔에 찍어서 찾아보면 req.params객체에 요청하려는 입력한 값이 담기는 것을 볼 수 있을 것이다.

//생략

findById: (req, res) => {
    if(req.params){
      const list = flights.filter((item) => {
        return item.uuid === req.params.id;
      });
      return res.status(200).json(list);
    }
  }

//생략

다음은 PUT 기능을 구현해야 한다. 즉, 요청 된 id 값과 동일한 uuid 값을 가진 데이터를 요청 된 body 데이터로 수정해야 한다. 그럼 입력된 id값과 동일한 데이터만 필터해서 그 데이터를 수정해주면 되지 않을까 해서 코드를 짜보았다.

//생략

update: (req, res) => {
    let data;
    if(req.params){
      data = flights.filter((el) => el.uuid === req.params.id)
      let body = Object.keys(req.body); 
        if(req.body){
          for(let i = 0; i < body.length; i++){
            data[0][body[i]] = req.body[body[i]]
          }
        }
        return res.status(200).json(data[0]);
      }
    }
  };

근데 위의 코드를 완전 간결하게 해줄 메소드가 하나 있다. Object.assign이다. Object.assign 사용법은 여기를 참고하면 된다.

//생략

update: (req, res) => {
    let data;
    if(req.params){
      data = flights.filter((el) => el.uuid === req.params.id)
      let list = Object.assign(data[0], req.body); //data[0]해준 이유는 data는 지금 하나의 객체를 가지고 있는 배열이기 때문.
        return res.status(200).json(list);
      }
    }
  };

👉🏻bookController

앞의 flightController를 구현하는 것과 거의 대부분이 동일하기 때문에 상세한 설명은 생략하겠다.

그럼 POST부터 구현해보자. 매우 간단하다. 항공편 예약 데이터를 저장할 booking에다가 담아주기만 하면 된다.

let booking = []
//생략

create: (req, res) => {
      booking.push(req.body);
      return res.status(201).json({ message: 'Create success!' });
  },

//생략

다음은 GET 구현이다. 앞서 flightController 구현과 동일한 방식이기 때문에 설명은 생략하겠다.

//생략

findById: (req, res) => {
    let list = booking;
    if(req.query.flight_uuid){
      list = list.filter((item) => {
        return req.query.flight_uuid === item.flight_uuid;
      });
      return res.status(200).json(list);
    }
    if(req.query.phone){
      list = list.filter((item) => {
        return req.query.phone === item.phone;
      });
      return res.status(200).json(list[0]); //여기서 list[0]을 해준 이유는 응답으로 객체 형태가 나와야 하는데, list는 배열이기 때문에 list의 0번째 요소인 객체를 보내주는 것이다.
    }
    else return res.status(200).json(list);
  },

//생략

다음은 DELETE 구현이다. 요청 된 phone 값과 동일한 예약 데이터를 삭제해야하므로, 나는 요청 된 phone값과 다른 데이터만 필터링 하는 것으로 구현했다.

// 생략
  
  deleteById: (req, res) => {
    if(req.query){
      let list = booking;
      if(req.query.phone){
        list = list.filter((item) => {
          return req.query.phone !== item.phone;
        });
      }
      return res.status(200).json(list);
    }
  }
};

이렇게 하면 이번 과제에서 원하는 모든 기능들을 다 구현해 줄 수 있다.

반응형
LIST

BELATED ARTICLES

more