클라이언트(브라우저)에는 서버에 요청을 보내기 위해 fetch와 같은 HTTP 요청을 보내는 도구가 기본적으로 내장되어 있다. 브라우저에서는 fetch API가 내장모듈로 제공된다는 뜻이고, fetch를 이용해 HTTP 요청을 보내고 응답 데이터를 받아보는 연습을 비동기 파트를 배울 때 해보았다.
이전시간에 했었던 내용
터미널에서npm run server:part-3
으로 서버를 열고, 서버를 실행한 상태에서는 다음 URL을 통해 정보를 얻어왔었다
- 최신 뉴스: http://localhost:5000/data/latestNews
- 날씨 정보: http://localhost:5000/data/weather
fetch API는 Promise 형식으로 되어져 있는 것을 확인하고, 받아온 data의 형태를 파악하였다. 그리고 json()메소드를 사용하여 그 data를 JSON형태로 변환시켜보기도 하였다.
node.js는 HTTP 요청을 보내거나, 응답을 받을 수 있는 도구를 제공한다. HTTP 요청을 처리하고 응답을 보내주는 프로그램을 웹 서버(Web Server)라고 부릅니다.
지난번 비동기를 배울 때 node.js의 수많은 내장 모듈 목록을 한 번 열어서 봐본적이 있다(당시 파일을 읽어오는 메소드 readFile
을 사용하기 위해 파일 시스템 모듈(fs 모듈)을 직접 사용해본 적이 있다)
이번 학습시간엔 node.js의 http 모듈을 이용해 웹 서버를 만들어 보았다. node.js에서 파일을 읽거나 쓰기 위해 fs 모듈을 사용하듯이, 아래 방식으로 HTTP 요청과 응답을 다루기 위해 http 모듈을 사용한다.
const http = require('http');
HTTP 트랜잭션 해부라는 공식 가이드 문서를 통하여 기능을 구현해보았다.
1. 먼저 서버를 생성하고--> 호출하기
const http = require('http');
const PORT = 5000;
const ip = 'localhost';
const server = http.createServer((request, response) => {
// 여기서 작업이 진행됩니다!
});
모든 node 웹 서버 애플리케이션은 createServer
를 이용하여 웹 서버 객체를 만들어야 한다.(server라는 변수에 해당 웹 서버 객체 할당)
이 서버로 오는 HTTP 요청마다 createServer
에 전달된 함수가 한 번씩 호출된다.
HTTP 요청이 서버에 오면 node가 트랜잭션을 다루려고 request
와 response
객체를 전달하며 요청 핸들러 함수를 호출한다.
server.listen(PORT, ip, () => {
console.log(`http server listen on ${ip}:${PORT}`);
});
요청을 실제로 처리하려면 listen 메서드가 위에서 만들어 놓은 server 객체에서 호출되어야 한다. 대부분은 서버가 사용하고자 하는 포트 번호를 listen에 전달하기만 하면 된다.
공식문서에서 확인해보니 아래와 같이 간략하게 표현하였다.
const http = require('http');
http.createServer((request, response) => {
//...
}).listen(8080); // 이 서버를 활성화하고 8080 포트로 받습니다.
2. 요청 받기
핸들러에 전달된 request 객체는 ReadableStream 인터페이스를 구현하고 있다. 이 스트림에 이벤트 리스너를 등록하거나 다른 스트림에 파이프로 연결할 수 있다. 스트림의 'data'와 'end' 이벤트에 이벤트 리스너를 등록해서 데이터를 받을 수 있다. 각 'data' 이벤트에서 발생시킨 청크는 Buffer이다. 이 청크가 문자열 데이터라는 것을 알고 있다면 이 데이터를 배열에 수집한 다음 'end' 이벤트에서 이어 붙인 다음 문자열로 만드는 것이 가장 좋다.(-->공식문서 내용)
let body = [];
request.on('data', (chunk) => {
body.push(chunk); //데이터가 들어오면 콜백함수 실행
}).on('end', () => {
body = Buffer.concat(body).toString(); //데이터가 들어오는 게 끝나면 콜백함수 실행
// 여기서 `body`에 전체 요청 바디가 문자열로 담겨있습니다.
});
body
라는 변수에 요청 바디가 전부 문자열로 담겨있는 상태에서 각 경우에 따라 아래와 같이 조건문을 작성하여 응답 케이스를 나누어 주었다.
if (request.method === 'POST') { //POST요청 중에서
if (request.url === '/lower') { //url이 /lower인 경우
} else if (request.url === '/upper') {//url이 /upper인 경우
} else { //그 외의 요청(에러 응답)
}
}
if (request.method === 'OPTIONS') { //CORS 관련 헤더를 OPTIONS 응답에 적용
}
위에서 body
에 담아둔 문자열 형태의 전체 요청 바디를 각각의 경우에 따라, toLowerCase() or to UpperCase()
를 해주었다.
3. 응답바디
request의 메소드가 OPTION인 경우는 클라이언트가 preflight request를 보냈을 때이다.
preflight request 에 대한 응답 헤더는 아래와 같다.
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
};
아래 공식문서 내용에서 예시 코드를 참고하고 한 줄로 대체된 코드 형태를 가져와 사용해보았다.
response.statusCode = 200; //응답코드
response.setHeader('Content-Type', 'application/json');
// 주의: 위 두 줄은 다음 한 줄로 대체할 수도 있습니다.
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body };
response.write(JSON.stringify(responseBody));
response.end();
// 주의: 위 두 줄은 다음 한 줄로 대체할 수도 있습니다.
// response.end(JSON.stringify(responseBody))
--> 요청바디를 소문자로 바꿔서 응답하는 경우는 아래와 같다.
req.on('end', () => {
data = data.toLowerCase();
res.writeHead(201, defaultCorsHeader); //(에러코드, 응답헤더)
res.end(data);
});
여태껏 위 웹서버 코드를 짤 때는 클라이언트 & 서버 모두 같은 origin에서 그 결과를 확인해보았다.
CORS를 적용해보기 위하여 작업중인 레포지토리에서 아래와 같이 가상의 클라이언트를 만들어주었다.
그 다음 서버의 응답헤더에서 위에서 만들어준 가상의 클라이언트 주소를 적었다.
const defaultCorsHeader = {
'Access-Control-Allow-Origin': 'http://localhost:1234', //이 부분 변경
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Accept',
'Access-Control-Max-Age': 10
};
http://localhost:1234
로 접속하여(서버와 origin이 다른 클라이언트) POST요청을 해보았더니 CORS가 잘 적용됨을 확인할 수 있었다.
이 상태에서 다시 index.html로 접속하여 기존 클라이언트에서 POST요청을 하면 아래와 같이 CORS 에러가 발생한다
아래 MDN 내용을 보면.. 아래 조건을 모두 충족한다면 CORS preflight를 트리거하지 않는다. 이를 simple request라고 한다.
(출처: MDN)
추가적으로 클라이언트 코드(App.js)의 아래 요청 헤더를 수정해보았다. (Content-Type
을 text/plain
으로)
post(path, body) {
fetch(`http://localhost:5000/${path}`, {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'text/plain'
}
})
.then(res => res.json())
.then(res => {
this.render(res);
});
아래와 같이 preflight을 트리거하지 않고 바로 POST가 요청된 것을 알 수 있었다.