[HTTP/Server] SOP와 CORS (2) - nodejs 서버로 간단한 HTTP 요청과 응답 다루기

초코침·2023년 4월 6일
0

HTTP/Server

목록 보기
4/6
post-thumbnail

Node.js의 http 모듈로 웹 서버를 만들어 요청을 보내고 응답을 받는 과정에 대해 포스팅한다.

서버 코드는 server.js 파일에 작성했고, 간단한 html & js 파일을 만들어 요청을 보내고 응답을 확인해 봤다.

0. http 모듈 import

server.js 파일을 생성한 다음, http 모듈을 import 한다.

const http = require('http');

앞으로 코드는 모두 server.js에 작성한다.

1. 서버 만들기

서버로 사용할 ip 주소(호스트)와 port 번호를 상수로 선언한다.

const IP = 'localhost';
const PORT = 4999;

서버는 createServer 메서드로 만들 수 있고 다음과 같이 작성한다.

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

request는 서버가 받을 요청을 의미하고 response는 서버가 보낼 응답을 의미한다.


만든 서버를 실행시켰을 때 연결을 수신하기 위해 listen 메서드를 이용한다. 이때 첫 번째와 두 번째 인수로 사용할 port 번호와 ip 주소를 주고, 세 번째 인수로는 listening 리스너로써 사용할 콜백 함수를 넣어준다.

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

서버를 실행시키면 콘솔에 http server is listening on localhost:4999가 출력될 것이다.

2. 서버 실행하기

서버는 터미널에 다음 명령어를 입력해 실행시킬 수 있다.

node {server.js 경로}

위 명령어로 서버를 실행한 다음 서버 코드를 수정한다면, 수정 내용을 반영하기 위해 서버를 종료하고 다시 실행시켜야 하는 번거로움이 있다.

이런 경우, nodemon 패키지를 사용하면 서버 코드를 수정한 후 다시 실행시킬 필요 없이 자동으로 변경된 내용의 서버 코드가 다시 실행된다.

nodemon 패키지는 다음처럼 설치한다.

npm install nodemon

그리고 package.json 파일의 “scripts”에 다음 코드를 추가한다.

"start": "nodemon {server.js 경로}"

그리고 터미널에 npx 명령어를 입력하면 변화를 자동으로 반영하는 서버가 실행된다.

npx nodemon {server.js 경로}

3. preflight 요청에 대한 응답 처리하기

preflight 요청에서 공부했듯이, 클라이언트에서 OPTIONS 메서드로 preflight 요청을 보내면 서버는 CORS에 관한 필드를 응답 헤더에 작성해 보내줘야 한다.

이에 관한 내용은 createServer 메서드의 콜백 함수 내부에서 처리한다.

OPTIONS 요청 걸러내기

요청의 메서드는 request.method로 확인할 수 있다.

const server = http.createServer((request, response) => {
  if (request.method === 'OPTIONS') {
    // TODO: preflight 요청에 대한 응답 작성
  }
});

CORS 관련 헤더 작성하기

서버가 응답 헤더에 담아야 할 CORS 필드는 다음과 같다.

  • Access-Control-Allow-Origin: 이 서버에게 요청을 보낼 수 있는 출처
  • Access-Control-Allow-Methods: 이 서버에게 보낼 수 있는 요청 메서드
  • Access-Control-Allow-Headers: 이 서버에서 허용하는 헤더 목록
  • Access-Control-Max-Age: 이 preflight 응답을 몇 초간 캐시할 것인지에 대한 초 단위 시간

위 내용을 객체로 작성한다.

const CORSHeader = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Accept',
  'Access-Control-Max-Age': 0,
};

응답 헤더 구성하기

response.writeHead 메서드로 상태 코드와 헤더에 보낼 내용인 CORSHeader를 붙여준다. 그리고 응답을 종료하기 위해 response.end 메서드를 호출한다.

const server = http.createServer((request, response) => {
  if (request.method === 'OPTIONS') {
    response.writeHead(200, CORSHeader);
    response.end();
  }
});

Postman으로 서버에 OPTIONS 요청을 보내면 응답 헤더에 붙여줬던 CORSHeader의 내용이 그대로 담겨져 왔음을 확인할 수 있다.

4. POST 요청에 대한 응답 처리하기

클라이언트로부터 POST 요청을 받으면 서버는 요청의 body 내용을 응답 메시지로 보내주는 작업을 할 것이다.

POST 요청 걸러내기

OPTIONS 요청을 걸러낸 것과 마찬가지로 request.method를 조회해 걸러낸다.

const server = http.createServer((request, response) => {
  if (request.method === 'OPTIONS') { /* ... */ }
	else if(request.method === 'POST') {
		// TODO: POST 요청에 대한 응답 작성
	}
});

요청의 body 가져오기

node.js 문서를 보면, request의 body는 다음 코드로 가져올 수 있다고 나와있다.

let body = [];
request
  .on('error', (err) => {
    console.error(err);
  })
  .on('data', (chunk) => {
    body.push(chunk);
  })
  .on('end', () => {
    body = Buffer.concat(body).toString();
    // 여기서 헤더, 메서드, url, 바디를 가지게 되었고
    // 이 요청에 응답하는 데 필요한 어떤 일이라도 할 수 있게 되었습니다.
  });

위 코드를 인용해 if 문 내부를 채워보자.

const server = http.createServer((request, response) => {
  if (request.method === 'OPTIONS') { /* ... */ }
	else if(request.method === 'POST') {
		let body = [];
    request
      .on('error', (error) => {
        console.error(error);
      })
      .on('data', (chunk) => {
        body.push(chunk);
      })
      .on('end', () => {
        body = Buffer.concat(body).toString();
				console.log(body); // 요청 body의 내용이 출력된다.
      });
	}
});

응답 헤더 구성하기

OPTION의 응답 헤더를 구성했던 것과 똑같이 적어준다.

이때 end 메서드의 인수로 응답 body에 담을 내용을 전달한다.

const server = http.createServer((request, response) => {
  if (request.method === 'OPTIONS') { /* ... */ }
	else if(request.method === 'POST') {
		let body = [];
    request
      .on('error', (error) => {
        console.error(error);
      })
      .on('data', (chunk) => {
        body.push(chunk);
      })
      .on('end', () => {
        body = Buffer.concat(body).toString();
				response.writeHead(200, CORSHeader);
        response.end(body);
      });
	}
});

Postman으로 POST 요청을 보내면 요청 body 내용이 응답 body로 똑같이 전달되었음을 확인할 수 있다.

[그냥 해 본 번외] CORS header 값 바꿔보기

Access-Control-Allow-Origin 바꿔보기

허용하는 Origin을 모두(*)로 설정하지 않고 다른 것으로 바꿔봤다.

const CORSHeader = {
  'Access-Control-Allow-Origin': 'http://only.this.url.can.request:4999',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Accept',
  'Access-Control-Max-Age': 0,
};

요청하려는 origin(http://localhost:4999)과 비교하면 프로토콜과 포트 번호는 같지만 호스트가 다르기 때문에 허용하지 않는 출처일 것이다.

결과

바로 CORS 오류 발생!

Access-Control-Allow-Origin의 값은 'http://only.this.url.can.request:4999'라서

요청을 보낸 출처인 http://localhost:5500과 일치하지 않(access control check를 통과하지 못했)기 때문에

요청은 http://localhost:4999에 보낼 수 없다는 뜻!

Access-Control-Allow-Methods 바꿔보기

허용하는 메서드 목록에서 POST 메서드를 지우고 POST 요청을 보내봤다.

const CORSHeader = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Accept',
  'Access-Control-Max-Age': 0,
};

결과

preflight 응답 헤더에 POST 메서드가 없는 것을 확인했는데도

본 요청에 대한 응답 페이로드에 요청 body에 작성한 내용이 담겨 왔다.

그럼 Access-Control-Allow-Methods는 메서드 제한을 못 두는 것 아닌가?

그래서 Access-Control-Allow-Methods에 대해 찾아봤더니,

If request’s method is not in [Access-Control-Allow-Methods] methods, request’s method is not a CORS-safelisted method, and request’s credentials mode is "include" or methods does not contain ”*”, then return a network error.

요청의 메서드가 Access-Control-Allow-Methods 목록에 없는 경우,
1. CORS 안전 목록에 나열된 메서드가 아니고
2. 요청의 credentials mode가 include거나 메서드에 *가 포함되지 않는다면
네트워크 에러를 던진다.

이런 내용이 있었다. 즉, CORS는 기본적으로 safe하다고 여기는 메서드들이 있다.

내가 삭제했던 POST 메서드는 CORS-safelisted method에 해당하는 메서드라, Access-Control-Allow-Methods에서 제외시켰더라도 문제 없이 요청이 가능하다. (충격)

Access-Control-Allow-Headers 바꿔보기

이번에는 Access-Control-Allow-Headers에서 Content-Type을 지우고 이를 포함시킨 요청을 보냈다.

const CORSHeader = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Accept',
  'Access-Control-Max-Age': 0,
};

결과

CORS 에러가 발생했다.

요청 헤더의 content-type 필드는 Access-Control-Allow-Headers에 의해 허용되지 않은 헤더라 요청을 보낼 수 없다.

Access-Control-Max-Age 바꿔보기

캐시되는지 확인해보기 위해 20초간은 캐시하도록 변경해 봤다.

const CORSHeader = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Accept',
  'Access-Control-Max-Age': 20,
};

그리고 요청을 보내자마자 동일한 요청을 한 번 더 보내봤다.

결과

두 번째 보낸 요청은 preflight 요청을 보내지 않은 것을 확인했다.

여담…

Postman을 통해 CORS 에러를 확인하려다보니 예상과 다르게 작동하는 게 있었다.

Access-Control-Allow-Headers에서 content-type 필드를 뺀 다음에 content-type이 포함된 요청을 보냈을 때 예상과 달리 에러가 발생하지 않았다.

그런데 생각해보니까 이 CORS 체크는 브라우저가 하는 일

그러니 Postman에서 단순히 POST 요청을 보냈을 때, 서버는 그에 해당하는 응답을 주고 Postman은 그 응답을 나에게 보여준 것… 심져 OPTIONS 요청도 아님…. 따라서 Postman 잘못 없음…. 내 잘못임

그래서 브라우저에서 다시 돌려봤는데 CORS 에러가 발생했다. ㅋㅋ

그래서 혹시나 하고 Access-Control-Allow-Methods 바꿔보기 작업도 Postman이랑 브라우저에서 모두 돌려봤는데 이건 결과가 동일했다(CORS 검사하는 브라우저에서도 에러를 내지 않기 때문).


끝!


서버 전체 코드 보러가기

profile
블로그 이사중 🚚 (https://sungjihyun.vercel.app)

0개의 댓글