3일동안 서버를 만들기 위한 node.js의 http모듈과 express모듈에 대해 공부했다.
간단한 서버를 http모듈을 이용해 만들어보고 express모듈을 이용한 리팩토링해보는 실습을 진행했다.
http모듈이란 http웹 서버와 관련된 모든 기능을 담은 모듈이다.
http모듈의 createServer메서드를 이용하여 server객체를 생성할 수 있다.
createServer의 인자로 request와 response를 매개변수로 갖는 함수를 전달한다.
생성된 서버로 요청이 올때마다 인자로 전달된 함수가 한번씩 호출된다.
const http=require('http')
const server=http.createServer(function(req,res){
//요청을 받아 응답을 작성하는 코드
})
해당 함수에는 서버가 동작할 때 요쳥(request)를 받아 어떻게 응답한지(response)를 기술한다.
createServer로 서버객체를 생성하고 생성된 서버객체의 listen메서드를 이용하면 서버객체는 클라이언트의 요청을 기다리며 대기하는 상태가된다.
//server.listen(port,callback)
server.listen(3000,()=>{
console.log('server running')
})
createServer의 인자로 전달되는 함수에서 request객체의 method프로퍼티를 이용해 요청의 HTTP method를 얻을 수 있다.(GET,POST등)
request객체의 url프로퍼티는 요청의 URL중 프로토콜,호스트,포트 부분을 제외한 부분을 값으로 갖는다.
요청이 GET일 경우 쿼리를 파싱하기 위해 url모듈의 parse메서드를 활용하여 요청의 쿼리값을 사용할 수 있다.
url.parse메서드가 반환한 Url객체의 query프로퍼티를 이용하여 쿼리값을 사용할 수 있다.
요청이 본문에 데이터를 포함하는 POST혹은 PUT메서드일 경우, 다음과 같이 본문의 데이터를 얻을 수 있다.
let body = [];
request.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
// 여기서 `body`에 전체 요청 바디가 문자열로 담겨있습니다.
});
//출처: https://nodejs.org/ko/docs/guides/anatomy-of-an-http-transaction/
response객체의 writeHead메서드를 이용하여 요청에 반환될 응답의 헤더를 작성할 수 있다.
response객체의 end메서드를 이용하여 본문을 작성하고 응답을 반환함으로써 통신을 끝낼 수 있다.
res.writeHead(상태코드,응답헤더);
res.end(응답 본문 데이터);
fs모듈의 readFile메서드를 이용해 서버디렉토리에 있는 html파일을 반환할 수 있다.
fs.readFile(파일의 path,(err,data)=>{
res.writeHead(200,{'Content-Type':'text/html'});
res.end(data);
})
http모듈을 이용해서 웹서버를 구축할때 createServer메서드의 인자로 전달되는 함수에 모든요청에 대한 응답작성방법을 기술해야한다.
const http=require('http')
const server=http.createServer(function(req,res){
if(req.method==='GET'){
//요청이 GET메서드일때 응답하는 코드
...
if(req.url===''){
//GET요청중에서도 url에 따라 분기해야 할수도 있다.
...
}
...
}
if(req.method==='POST){
//요청이 POST메서드일때 응답하는 코드
}
...
})
따라서 코드가 복잡해지고 가독성도 떨어지는 단점이 있다.
express모듈은 기존의 웹서버를 생성하기 위한 http모듈에 여러가지 기능을 추가해서 사용할 수 있게 만든 모듈이다.
기존의 http모듈을 이용하여 웹서버를 생성할 때는 createServer의 인자로 전달되는 함수로 모든 요청을 처리했다.
따라서 하나의 함수안에 요청의 method와 url을 기준으로 여러번 분기하여 다른 응답을 할 수 있도록 코드를 작성해야했다.
express는 특정 method와 특정 endpoint에 대한 요청을 응답하는 라우팅의 기능을 제공한다.
Server객체.HTTP method(endpoint,function(req,res){...})
$ npm install express
http모듈은 node자체에 내장되어 있어 따로 설치할 필요가 없었지만 express는 직접 설치하여 사용해야한다.
const express=require('express')
//서버객체 생성
const server=express();
server.get('/',function(req,res){
// '/'엔드포인트로 GET요청이 들어올때 응답하는 함수
...
})
server.post('/create',function(req,res){
//. '/create'엔드포인트로 POST요청이 들어올때 응답하는 함수
...
})
...
미들웨어란 클라이언트에서 서버로 요청이 들어오면 해당 요청에 응답하는 과정의 중간에 목적에 맞게 처리를 하는, 중간에 거쳐가는 함수들을 의미한다.
미들웨어 함수는 req 요청객체, res 응답객체, 다음의 미들웨어 함수에 접근할 수 있는 next함수를 인자로 갖는다.
미들웨어 함수는 다음과 같은 태스크를 수행할 수 있다.
현재 미들웨어함수가 요청-응답 주기를 종료하지 않는다면 반드시 마지막에 next함수를 호출하여 다음 미들웨어 함수를 호출해야한다.
그렇지 않으면 요청이 서버에서 정지된 채로 방치된다.
위의 그림에서 보이듯이 요청메서드와 엔드포인트에 맞춰 미들웨어를 설정할 수 있다.(출처:express 미들웨어 공식문서)
만약 서버의 모든 요청에 미들웨어 함수를 적용시키고 싶다면 다음과 같이 코드를 작성하면 된다.
//서버객체.use(미들웨어함수)
server.use(function(req,res,next){
...
})
http모듈을 이용해 생성한 웹서버에서는 요청의 본문을 파싱하기 위해서는 Buffer를 조합하는 다소 복잡한 코드가 필요했다.(http모듈의 내용에 있다.)
하지만 express모듈은 bodyParser라는 미들웨어를 내장하고 있어 한줄의 코드만으로 요청객체의 본문을 파싱할 수 있다.
(예전엔 body-parser라는 모듈이 따로 있었지만 express의 버전에 따라 express에 내장되었다고 한다.)
app.use(express.json()) //요청의 본문타입이 application/json일때 본문을 파싱한다.
app.use(express.text()) //요청의 본문타입이 text일때 본문을 파싱한다.
위의 코드를 작성하게 되면 모든 요청에 대해 본문을 파싱할 수 있고 파싱된 본문은 req 요청객체의 body프로퍼티로 접근이 가능하다.(req.body)
express공식문서
전에 공부했던 브라우저의 cors정책을 위반하지 않기위해 http모듈을 이용해 웹서버를 생성할 때는Access-Control-Allow-Origin헤더를 작성하고 preflight request를 위한 OPTIONS메서드를 위한 응답 작성부도 작성해야한다.(복잡하다.....ㅠ)
const http=require('http');
//cors 헤더작성
const corsHeader = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Accept',
};
const server=http.createServer(function(req,res){
if(req.method==='GET'){
res.writeHead(200,corsHeader)
..
}
if(req.method==='POST'){
res.writeHead(200,corsHeader)
..
}
...
if(req.method==='OPTIONS'){
res.writeHead(200,corsHeader)
..
}
...
})
express모듈은 cors모듈을 이용해 cors를 쉽게 처리할 수 있다.
다만 body-parser와 다르게 내장되어 있지는 않기 때문에 따로 install해줘야 한다.
$ npm install cors
cors모듈 공식문서
app.use(cors())
위의코드로 모든 요청에 대해 cors처리를 하게되어 모든 도메인에서 제한없이 서버에 요청을 보내고 응답을 받을 수 있다.
특정 도메인에 한정해서 요청을 받고 응답을 처리하고 싶다면 다음과 같이 코드를 작성한다.
var whitelist = [...]
//whitelist에 Access-Control-Allow-Origin처럼 요청을 허용할 도메인을 적는다.
var corsOptions = {
origin: function (origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('not allowed domain!'))
}
}
}
app.use(cors(corsOptions))
http모듈을 이용한 웹서버에서는 html문서를 반환하기 위해서는 fs모듈의 readFile메서드를 이용해야 했지만 express모듈은 res응답객체의 sendFile메서드를 이용하여 쉽게 파일을 전송할 수 있다.
또한 http모듈을 이용한 웹서버에서는 html과 관련된 css,js파일등을 요청하기 위해 html문서의 link태그와 script태그의 주소를 바꿔주고 서버도 해당 주소의 요청에 알맞은 파일을 전송해주는 응답부를 작성해야 했지만 express모듈은 express.static을 이용하여 쉽게 정적파일을 전달해 줄 수 있다.
다음과 같이 디렉토리에 파일이 있다.
index.html은 전송할 html파일이며, app.js는 index.html에 적용될 스크립트 파일이다.
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" href="data:,">
<meta charset="UTF-8">
<title>HTML Page</title>
</head>
<body>
<div>
<h1>Hello World!!</h1>
</div>
<button>Click!</button>
<!--express모듈을 이용한 웹서버 js파일요청 -->
<script src='./app.js'></script>
<!--http모듈을 이용한 웹서버에 js파일 요청-->
<!-- <script src='http://localhost:3000/js' type="text/javascript"></script> -->
</body>
</html>
//app.js
const header=document.querySelector('h1');
const btn=document.querySelector('button');
let cnt=0;
btn.addEventListener('click',function(){
cnt++;
header.textContent=`${cnt} Clicked!`;
})
//httpserver.js
const http=require('http');
const path=require('path');
const fs=require('fs');
//서버 객체 생성
const server=http.createServer(function (req,res){
if(req.method==='GET'){
if(req.url==='/js'){
fs.readFile(path.resolve(__dirname,'../pages/app.js'),(err,data)=>{
if(err){
console.log(err);
res.writeHead(400);
res.end();
}
res.writeHead(200,{'Content-Type':'text/javascript'});
res.end(data);
})
}
if(req.url==='/'){
fs.readFile(path.resolve(__dirname,'../pages/index.html'),(err,data)=>{
if(err){
console.log(err);
res.writeHead(400);
res.end();
}
console.log(data.toString());
res.writeHead(200,{'Content-Type':'text/html'});
res.end(data);
})
}
}
});
server.listen(3000,()=>{
console.log('server running')
})
html파일을 보내줄 것인지, js파일을 보내줄 것인지 url에 따라 다른 응답을 한다.
(일반적으로 브라우저에 주소를 입력해 이동하는 것은 GET메서드)
//express.js
const express=require('express');
const path=require('path');
const server=express();
server.use(express.static(path.resolve(__dirname,'../pages')));
server.get('/',(req,res)=>{
res.sendFile(path.resolve(__dirname,'../pages/index.html'));
})
server.listen(5000,()=>{
console.log('express server running')
})
express.static메서드와 경로를 지정하기 위한 path모듈을 사용하여 코드가 훨씬 간결해졌다.
직접 실습을 해보니 express모듈이 http모듈보다 훨씬 편리하고 다양한 기능이 있다고 느꼈다.
요청과 응답이 어떻게 이루어지는지 http message가 어떻게 전달되는지를 잘 생각하면서 서버를 구축하는 연습을 해봐야 할 것 같다.
직접 다뤄본 메서드들과 작성한 코드는 진짜 기본밖에 안되는 것 같다.
좀 더 네트워크와 서버에 대해 공부를 해봐야 할 것 같다.