본문 바로가기

Javascript

HTTP server만들기 ( CORS , mini node server )

우리가 어떤 사이트에서 로그인 버튼을 눌렀을 때, 서버에서 개인정보를 가져와서 로그인이 된다

 

이 처럼 우리는 클라이언트와 서버에 익숙해져있다

 

그럼 클라이언트는 무엇이고 서버는 무엇일까??

 

카페를 비유로 들자면

 

커피를 주문하는 사람을 클라이언트,

주문을 받아서 상품을 만들어 주는 것을 서버라고 할 수 있겠다

 

근데 만약 손님이 이상한 외계어로 주문을 한다면?????

주문을 받는 사람은 그 말을 알아들을 수 가 없기 때문에 문제가 발생한다

 

이와 같이 클라이언트와 서버간의 소통을 원활히 하기 위해 만든 규칙을 프로토콜 이라고 한다

 

프로토콜 === 통신규약

 

웹 애플리케이션 아키텍쳐에서는 클라이언트와 서버가 서로 HTTP라는 프로토콜을 통해 상호 소통을 한다

 

오늘은 HTTP프로토콜을 기반으로 하는 서버를 만드는 것을 해보도록 하자

 

 

1. 클라이언트

 

class App {
  init() {
    document
      .querySelector('#to-upper-case')
      .addEventListener('click', this.toUpperCase.bind(this));
    document
      .querySelector('#to-lower-case')
      .addEventListener('click', this.toLowerCase.bind(this));
  }
  post(path, body) {
    fetch(`http://localhost:5000/${path}`, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: {
        'Content-Type': 'application/json'
      }
    })
      .then(res => res.json()) //서버에서 온 응답의 body를 해석한다
      .then(res => {
        this.render(res);
      });
  }
  toLowerCase() {
    const text = document.querySelector('.input-text').value;
    this.post('lower', text);
  }
  toUpperCase() {
    const text = document.querySelector('.input-text').value;
    this.post('upper', text);
  }
  render(response) {
    const resultWrapper = document.querySelector('#response-wrapper');
    document.querySelector('.input-text').value = '';
    resultWrapper.innerHTML = response;
    console.log('hellow')
  }
}

const app = new App();
app.init();

먼저 서버에게 요청을 하고 응답을 받아서 어떤 행동을 하는 클라이언트를 간단하게 만든다

 

이 클라이언트는 html에서 입력된 text내용을 가져와서 toUppercaver버튼을 누르면 내용을 대문자로 바꿔주고,

toLowercase버튼을 누르면 소문자로 바꿔주는 동작을 한다

 

text내용을 가져와서 서버에 내용을 POST요청하고 서버로부터 응답받은 내용을 DOM을 이용해서 html에 뿌려준다

 

그렇다면 서버에서 해야하는 일은 무엇일까??

 

1. 클라이언트로부터 요청을 받는다

2. 요청을 받았을 때(POST) 대문자로 바꿔달라는 내용인지('/upper'), 소문자로 바꿔달라는 내용인지 ('/lower')를 구분한다

3. 구분한뒤에 받은 text내용을 각각 해당되는 내용으로 바꿔서 다시 클라이언트로 보내준다

 

 

2. 서버

const http = require('http');

const PORT = 5000;

const ip = 'localhost';


const server = http.createServer((request, response) => {

  // 메소드가 post고, url이 /lower면 소문자로 바꿔서 클라이언트로 준다
  // 메소드가 POST고, url이 /upper면 대문자로 바꿔서 클라이언트로 준다
  // OPTIONS가 왔을 때는 CORS 관련 헤더를 응답에 적용한다

  if (request.method === 'POST') {

    if (request.url === '/upper') {
      let body = ''
      //대문자로 바꾸는 작업
      request.on('data', (chunk) => { //chunk는 문자열
        body = body + chunk
      }).on('end', () => {
        body = body.toUpperCase() //대문자로 바꿈
        response.writeHead(200, defaultCorsHeader);//헤드 보내주기
        response.end(JSON.stringify(body));//바디 보내주기
      })
    }
    else if (request.url === '/lower') {
      //소문자로 바꾸는 작업
      let body = ''
      request.on('data', (chunk) => {
        body = body + chunk
      }).on('end', () => {
        body = body.toLowerCase()
        // 다른 origin에서 오는 POST 요청이기 때문에 CORS허용 내용을 보내주기
        response.writeHead(200, defaultCorsHeader);//헤드
        response.end(JSON.stringify(body));//바디
      })
    }
    else {
      response.writeHead(404, defaultCorsHeader);//헤드
      response.end();//바디
    }
  }

  if (request.method === 'OPTIONS') {
    // prefliht request에 대한 응답을 준다
    response.writeHead(200, defaultCorsHeader);//헤드
    response.end();//바디
  }
  console.log(
    `http request method is ${request.method}, url is ${request.url}`
  );


});


server.listen(PORT, ip, () => {
  console.log(`http server listen on ${ip}:${PORT}`);
});

// 1. 모든 도메인은 모두 허용
// 2. 메소드는 GET, POST, PUT, DELETE, OPTIONS 중 하나인 경우에만 허용
// 3. 헤더는 Content-Type나 Accept 두 가지 경우에만 허용
// 4. prefligte 최대 허용 시간은 10초
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
};

 

request는 클라이언트로 부터 서버가 받은 요청 , response는 서버가 클라이언트에게 주는 응답이다

 

먼저 클라이언트로부터 받은 요청이 POST일 때, 두 가지 경우를 생각해야 한다

url이 /upper라면 받은 내용(data)를 대문자로 바꿔서 보내줘야하고 , url이 /lower하면 (data)를 소문자로 바꿔 보내줘야 한다

 

body라는 빈 문자열을 만들고, request.on을 해서 data 이벤트가 발생하면 (= 데이터를 잘 받았다는 얘기)

그 받은 데이터를 대문자로 바꿔주기 위해 body에 넣는다

 

여기서 받은 데이터를 chunk라고 했는데 이는 Buffer이고 문자열이다

결국 받은 문자열을 대문자로 바꾸기 위해 빈문자열에 잠시 넣어놓는 것이다

 

그리고 end이벤트를 통해 빈문자열에 넣은 값을 대문자로 바꿔주고 클라이언트에 그 값을 보내주면 된다

 

서버가 클라이언트에 값을 보내줄 때 head와 body를 같이 보내줘야 하는데

 

head에는 상태코드와 CORS 허용 내용을 넣어주어야 한다

헤드에 CORS허용 내용을 넣어주지 않으면, 만약 다른 도메인에서 오는 요청이 CORS 허용 조건들을 지키지 않았을 때

CORS오류가 난다

 

바로 이렇게...

 

오류 내용을 살펴보자면

CORS 정책: 요청된 리소스에 '액세스 제어 오리진 허용' 헤더가 없습니다

 

서버에서 클라이언트로 주는 헤더에 '액세스 제어 오리진 허용'이 없다는 오류가 난다

이는 다른 도메인에서 요청이 왔을 때 CORS 허용 조건을 다 만족시킬 때만 서버에서 요청을 받아준다는 것인데

이를 이해하기 위해서는 CORS가 무엇인지 알아야 한다

 

 

3. CORS

 

CORS

 

리소스 도메인과 다른 도메인으로부터 리소스가 요청될 경우에 원래는 허용을 안해주는데

CORS라는 규칙에 따르는 허용 범위 내에서는 다른 도메인에서 받은 요청이라도 허용해주는 것

 

그렇다면 CORS 종류에는 무엇이 있는지 알아보자

 

(1) simple requests

 

몆개의 request들은 CORS를 발생시키지 않는다

simple request라는 것은 CORS를 발생시키지 않는 조건들을 알려준다

 

1. GET , HEAD, POST 이 세 개의 메소드중에 무조건 하나여야 하고

2. 기본적으로 정의되는 헤드의 종류만 사용했을 때이다

    그중 Content-Type에는 text/plain 외 2가지 종류만 가능하다

 

아주 까다롭다

1,2번 조건 모두를 만족시켰을 때만 CORS가 발생하지 않는것이다

 

그렇지만 우리가 만든 클라이언트를 보면 POST요청을 하고는 있지만

Content-Type이 application/json이다

 

그러므로 우리가 만든 클라이언트는 simple request 조건들을 만족시키지 않기 때문에 CORS를 발생시킨다

 

 

2. preflighted request

 

preflighted requestsimple request와는 달리,

먼저 OPTIONS 메소드를 보내서 실제 요청이 전송하기에 안전한지 여부를 확인한다

Cross-site요청은 유저 데이터에 영향을 줄 수 있기 때문에 미리전송(prefflighted)한다

 

다른 도메인에서 요청이 왔을 때는 CORS라는 조건을 만족시키는 요청이라면 허용해주고, 아니면 허용을 해주지 않겠다는 내용이다

 

그러면 우리가 만든 클라이언트는 다른 도메인에서 서버로 요청을 하는 것이기 때문에 preflighted request 조건을 만족시켜야 한다

 

위 다섯가지 메소드를 쓴다면 무조건 preflighted request가 발생된다

 

그리고 아까 simple request가 일어나는 조건이었던 헤더 조건들과 맞지 않는 헤더를 사용하는 경우에도 preflighted request가 발생된다

preflighted request가 발생하면 POST요청이 가기전에 OPTIONS가 먼저 서버로 간다

그리고 CORS조건에 맞는다면 서버에서 클라이언트에게 가능하다는 응답을 준다

 

그리고 클라이언트는 OPTIONS에 대한 응답을 받은 뒤에야 본 요청인 POST요청을 한다

 

const http = require('http');

const PORT = 5000;

const ip = 'localhost';


const server = http.createServer((request, response) => {

  // 메소드가 post고, url이 /lower면 소문자로 바꿔서 클라이언트로 준다
  // 메소드가 POST고, url이 /upper면 대문자로 바꿔서 클라이언트로 준다
  // OPTIONS가 왔을 때는 CORS 관련 헤더를 응답에 적용한다

  if (request.method === 'POST') {

    if (request.url === '/upper') {
      let body = ''
      //대문자로 바꾸는 작업
      request.on('data', (chunk) => { //chunk는 문자열
        body = body + chunk
      }).on('end', () => {
        body = body.toUpperCase() //대문자로 바꿈
        response.writeHead(200, defaultCorsHeader);//헤드 보내주기
        response.end(JSON.stringify(body));//바디 보내주기
      })
    }
    else if (request.url === '/lower') {
      //소문자로 바꾸는 작업
      let body = ''
      request.on('data', (chunk) => {
        body = body + chunk
      }).on('end', () => {
        body = body.toLowerCase()
        // 다른 origin에서 오는 POST 요청이기 때문에 CORS허용 내용을 보내주기
        response.writeHead(200, defaultCorsHeader);//헤드
        response.end(JSON.stringify(body));//바디
      })
    }
    else {
      response.writeHead(404, defaultCorsHeader);//헤드
      response.end();//바디
    }
  }

  if (request.method === 'OPTIONS') {
    // prefliht request에 대한 응답을 준다
    response.writeHead(200, defaultCorsHeader);//헤드
    response.end();//바디
  }
  console.log(
    `http request method is ${request.method}, url is ${request.url}`
  );


});


server.listen(PORT, ip, () => {
  console.log(`http server listen on ${ip}:${PORT}`);
});

// 1. 모든 도메인은 모두 허용
// 2. 메소드는 GET, POST, PUT, DELETE, OPTIONS 중 하나인 경우에만 허용
// 3. 헤더는 Content-Type나 Accept 두 가지 경우에만 허용
// 4. prefligte 최대 허용 시간은 10초
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
};

 

 

그래서 다시 코드로 돌아가서 보면 end로 서버에서 클라이언트로 응답을 주는데 헤드에는 상태코드와 CORS관련 내용을 같이 넣어서 보내주는 것이다 (이제는 왜 상태코드와 함께 CORS관련 응답을 주는지 이해가 될 것이다)

 

그리고 만약 preflighted request가 발생하여 클라이언트로부터 OPTIONS가 먼저 왔을 때는

그에 대한 응답으로 헤드에 상태코드와 CORS관련 내용을 보내준다

 

그리고 서버가 실행되기 위해서는 listen이라는 메소드가 있어야 하고, listen이 실행되어야 서버가 실제로 실행이 된다

이 listen메소드는 일반적으로 ip,port번호가 들어간다

 

맨밑에 defaultCorsHeader는 CORS 에 관한 내용을 객체로 한번에 담아 넣은것이다

 

차례대로 보면

1. 모든 도메인을 허용한다 (다른 도메인에서 오는 요청이라도 일단 받겠다 , 하지만 밑에 조건들을 모두 만족시켜야 할 것이다)

2. 메소드는 GET,POST,PUT,DELETE,OPTIONS 이 다섯개 메소드중에 하나여야 한다

3. 헤더는 Content-Type이나 Accept 두 가지만 허용하겠다

4. preflighted 허용 시간은 최대 10초이다

 

(결론)

 

1. 클라이언트는 서버에 POST요청을 하는데 , 다른 도메인에서 하는 요청이고

헤더가 Content-Type: application/json이라서 CORS 종류중 preflighted request를 발생시킨다

 

2. 그래서 POST요청이 가기전에 먼저 서버에 OPTIONS가 날아간다

 

3. 서버는 OPTIONS가 먼저 날아왔기 때문에 CORS관련 내용을 헤드에 넣어서 응답을 준다

 

4. 서버로 부터 OPTIONS에 관한 응답을 받은 클라이언트는 CORS관련 조건들을 모두 만족하고 있다면

그제서야 서버에게 POST요청을 한다

 

5. 서버는 POST요청을 받았고 url로 구분을 해서 소문자로 바꿀지, 대문자로 바꿀지 결정한다

 

6. 그리고 소문자 혹은 대문자로 바꾼 내용을 body에 넣고, 상태코드와 CORS관련 내용을 헤드에 넣어서 클라이언트에게 돌려준다

 

7. 클라이언트는 서버에게서 받은 응답중 body 부분을 response.json()으로 변환해서 그 내용을 DOM을 이용해 html에 뿌려준다