Node.js의 http 모듈로 웹 서버를 만들어 요청을 보내고 응답을 받는 과정에 대해 포스팅한다.
서버 코드는 server.js 파일에 작성했고, 간단한 html & js 파일을 만들어 요청을 보내고 응답을 확인해 봤다.
server.js
파일을 생성한 다음, http
모듈을 import 한다.
const http = require('http');
앞으로 코드는 모두 server.js
에 작성한다.
서버로 사용할 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
가 출력될 것이다.
서버는 터미널에 다음 명령어를 입력해 실행시킬 수 있다.
node {server.js 경로}
위 명령어로 서버를 실행한 다음 서버 코드를 수정한다면, 수정 내용을 반영하기 위해 서버를 종료하고 다시 실행시켜야 하는 번거로움이 있다.
이런 경우, nodemon
패키지를 사용하면 서버 코드를 수정한 후 다시 실행시킬 필요 없이 자동으로 변경된 내용의 서버 코드가 다시 실행된다.
nodemon
패키지는 다음처럼 설치한다.
npm install nodemon
그리고 package.json
파일의 “scripts”
에 다음 코드를 추가한다.
"start": "nodemon {server.js 경로}"
그리고 터미널에 npx 명령어를 입력하면 변화를 자동으로 반영하는 서버가 실행된다.
npx nodemon {server.js 경로}
preflight 요청에서 공부했듯이, 클라이언트에서 OPTIONS
메서드로 preflight 요청을 보내면 서버는 CORS
에 관한 필드를 응답 헤더에 작성해 보내줘야 한다.
이에 관한 내용은 createServer 메서드의 콜백 함수
내부에서 처리한다.
요청의 메서드는 request.method
로 확인할 수 있다.
const server = http.createServer((request, response) => {
if (request.method === 'OPTIONS') {
// TODO: preflight 요청에 대한 응답 작성
}
});
서버가 응답 헤더에 담아야 할 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의 내용이 그대로 담겨져 왔음을 확인할 수 있다.
클라이언트로부터 POST 요청을 받으면 서버는 요청의 body 내용을 응답 메시지로 보내주는 작업을 할 것이다.
OPTIONS 요청을 걸러낸 것과 마찬가지로 request.method
를 조회해 걸러낸다.
const server = http.createServer((request, response) => {
if (request.method === 'OPTIONS') { /* ... */ }
else if(request.method === 'POST') {
// TODO: POST 요청에 대한 응답 작성
}
});
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로 똑같이 전달되었음을 확인할 수 있다.
허용하는 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
에 보낼 수 없다는 뜻!
허용하는 메서드 목록에서 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
에서 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
에 의해 허용되지 않은 헤더라 요청을 보낼 수 없다.
캐시되는지 확인해보기 위해 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 검사하는 브라우저에서도 에러를 내지 않기 때문).
끝!