본문 바로가기

NodeJS/Do it Node.js 내용정리

[ do it NodeJS - 4-1] 웹서버 만들기

웹 서버 만들기

4-1. 간단한 웹 서버 만들기

4-2. 익스프레스로 웹 서버 만들기

4-3. 미들웨어 사용하기

4-4. 요청 라우팅 하기

4-5. 쿠키와 세션 관리하기

4-6. 파일 업로드 기능 만들기

 


4-1. 간단한 웹 서버 만들기

노드에 기본으로 들어있는 http 모듈을 사용하면 웹 서버 기능을 담당하는 서버 객체를 만들 수 있습니다. http 모듈을 로딩했을 때 반환되는 객체에는 createServer()메소드가 정의되어 있습니다. 따라서 이 메소드를 호출하면 서버 객체를 만들 수 있습니다.

 

const http = require('http');

const server = http.createServer();

const port = 3000;

server.listen(port, () => {
  console.log('Running : %d', port);
})

http 모듈에 들어 있는 서버 기능을 사용하려면 require() 메소드로 http 모듈을 불러옵니다. 모듈과 똑같은 http 변수를 만든 후 이 변수에 할당합니다. 

 

이 http 객체의 createServer() 메소드를 호출하면 서버 객체가 반환됩니다. 이 서버 객체의 listen() 메소드를 호출하면 웹 서버가 시작됩니다.

 

서버를 시작할 때는 포트를 3000번으로 지정하여 해당 포트에서 클라이언트의 요청을 대기합니다. listen() 메소드를 호출할 때 전달하는 두 번째 파라미터는 콜백 함수로, 웹 서버가 시작되면 호출됩니다.

 

웹 서버는 일반적으로 웹 브라우저라고 하는 클라이언트에서 HTTP 프로토콜로 요청한 정보를 처리한 후 응답을 보내 주는 역할을 합니다. 이때 PC나 서버 기계에 있는 6만개가 넘는 포트들 중에서 하나를 사용하는데 여기에서는 3000번을 사용합니다.

 

웹 서버를 만들고 실행하는 과정을 보면, 먼저 createServer() 메소드를 호출하여 웹 서버 객체를 만들고, 그 객체의 listen() 메소드를 호출하여 특정 포트에서 대기하도록 설정합니다. createServer() 메소드로 만든 서버 객체에서 사용할 수 있는 대표적인 메소드는 다음과 같습니다.

 

listen ( port, [hostname], [backlog], [callback] )  : 서버를 실해아여 대기시킵니다.

close ( [callback] ) : 서버를 종료합니다.

 

서버 기계에 이더넷 카드가 여러개 있는 경우, 서버에서 사용 할 수 있는 인터넷 주소, 즉 IP 주소가 여러 개 존재할 수 있습니다. 이렇게 IP 주소가 여러 개 있을 때는 IP를 지정해서 서버를 실행해야 할 때도 있습니다. 이런 경우에는 listen() 메소드를 호출하면서 IP를 직접 지정합니다. 다음 코드를 사용하면 IP와 함께 백로그(실질적으로 동시 접속 연결 수를 결정하는 정보)를 지정할 수 있습니다.

 

 

클라이언트가 웹 서버에 요청할 때 발생하는 이벤트

connection : 클라이언트가 접속하여 연결이 만들어질 때 발생하는 이벤트

request : 클라이언트가 요청할 때 발생하는 이벤트

close : 서버를 종료할 때 발생하는 이벤트

 

const http = require('http');

const server = http.createServer();

const port = 3000;
server.listen(port, () => {
  console.log('Server Running! : %d', port);
});

server.on('connection', (socket) => {
  const addr = socket.address();
  console.log('클라이언트 접속 : %s, %d', addr.address, addr.port);
});

server.on('request', (req, res) => {
  console.log('클라이언트 요청');
  console.dir(req);
});

server.on('close', () => {
  console.log('서버가 종료');
});

웹 브라우저와 가은 클라이언트가 웹 서버에 연결되면 connection 이벤트가 발생합니다. 그러므로 on() 메소드를 호출할 때 첫번째 파라미터로 connection  이벤트 이름을 전달하고, 두번쨰 파라미터로 콜백 함수를 전달합니다. 연결이 만들어져 콜백 함수가 호출될 때는 Socket 객체가 파라미터로 전달됩니다. 이 객체는 클라이언트 연결 정보를 담고 있으므로 address() 메소드를 호출하여 클라이언트의 IP와 포트 정보를 확인 할 수 있습니다.

 

클라이언트가 특정 패스로 요청하면 request 이벤트가 발생합니다. 이때도 앞에서 알아본 것처럼 on() 메소드를 사용해 이벤트를 처리합니다. 콜백 메소드로 전달되는 요청 객체를 console.dir() 메소드로 화면에 출력하면 어떤 정보가 들어 있는지 확인할 수 있습니다.

 

const http = require('http');

const server = http.createServer();

const port = 3000;
server.listen(port, () => {
  console.log('Server Running! : %d', port);
});

server.on('connection', (socket) => {
  const addr = socket.address();
  console.log('클라이언트 접속 : %s, %d', addr.address, addr.port);
});

server.on('request', (req, res) => {
  console.log('클라이언트 요청');
  res.writeHead(200, {"Content-Type" : "text/html; charset=utf-8"});
  res.write("<!DOCTYPE html>");
  res.write("<html>");
  res.write("<head>");
  res.write("<title>응답 페이지</title>");
  res.write("</head>");
  res.write("<body>");
  res.write("<h1>노드제이에스로부터의 응답 페이지</h1>");
  res.write("</body>");
  res.write("</html>");
  res.end();
});

server.on('close', () => {
  console.log('서버가 종료');
});

res 객체의 writeHead(), wirte(), end() 메소드를 사용하면 클라이언트로 응답을 보낼 수 있습니다. 이 중에서 end() 메소드는 응답을 모두 보냈다는 것을 의미하며, 일반적으로는 end() 메소드가 호출 될 때 클라이언트로 응답을 전송합니다. 이렇게 해서 클라이언트가 웹 서버에 요청을 보내고 응답을 받을 수 있는 기능을 만들어 보았습니다.

 

on( ) 메소드는 이벤트를 처리할 때 사용하는 가장 기본적인 메소드입니다. 이 메소드로 connection, request, close 이벤트를 처리할 수 있는 콜백 함수를 각각 등록해 두면 상황에 맞게 자동 호출됩니다. 클라이언트가 요청을 해 왔을 때 발생하는 request 이벤트를 처리할 수 있게 등록해 둔 콜백 함수에서는 res객체를 사용해서 클라이언트로 응답을 보냅니다. res 객체를 사용해서 응답을 보낼 때 사용하는 주요 메소드는 다음과 같습니다.

 

메소드

writeHead(statusCode [, statusMessage][, headers])

응답으로 보낼 헤더를 만듭니다.

 

write(chunk[, encoding][, callback]

응답 본문(body) 데이터를 만듭니다. 여러 번 호출될 수 있습니다.

 

end([data][, encoding][, callback])

클라이언트로 응답을 전송합니다. 파라미터에 데이터가 들어 있다면 이 데이터를 포함시켜 응답을 전송합니다. 클라이언트의 요청이 있을 때 한번은 호출되어야 응답을 보내며, 콜백 함수가 지정되면 응답이 전송된 후 콜백 함수가 호출됩니다.

 

const http = require('http');

const server = http.createServer( (req, res) => {
  console.log('클라이언트 요청');
  res.writeHead(200, {"Content-Type" : "text/html; charset=utf-8"});
  res.write("<!DOCTYPE html>");
  res.write("<html>");
  res.write("<head>");
  res.write("<title>응답 페이지</title>");
  res.write("</head>");
  res.write("<body>");
  res.write("<h1>노드제이에스로부터의 응답 페이지</h1>");
  res.write("</body>");
  res.write("</html>");
  res.end();
});

const port = 3000;
server.listen(port, () => {
  console.log('Server Running! : %d', port);
});

server.on('connection', (socket) => {
  const addr = socket.address();
  console.log('클라이언트 접속 : %s, %d', addr.address, addr.port);
});

server.on('request', (req, res) => {

});

server.on('close', () => {
  console.log('서버가 종료');
});

request 이벤트를 사용해서 클라이언트로 응답을 보내지 않고 서버 객체를 만들 때 사용한 createserver() 메소드 호출 부분에 응답 코드를 바로 입력 할 수도 있습니다. 파일을 실행하면 이전 예제와 결과가 똑같이 나타납니다. 이 방법은 request 이벤트를 처리하는콜백함수에서 응답을 보내는것과 다르지 않습니다. 때에 따라서 이렇게 만들수도 있다는 것을 알아두세요.

 

 

클라이언트에서 요청이 있을 때 파일을 읽어 응답하기

첫째 장에서 fs 모듈로 파일을 다루는 방법을 알아 보았으니  이제 클라이언트의 요청이 들어왔을 때 파일을 읽고 응답하는 방법을 알아보겠습니다. 

const http = require('http');
const fs = require('fs');

const server = http.createServer();

const port = 3000;
server.listen(port, () => {
  console.log('Server Running! : %d', port);
});

server.on('connection', (socket) => {
  const addr = socket.address();
  console.log('클라이언트 접속 : %s, %d', addr.address, addr.port);
});

server.on('request', (req, res) => {
  console.log('클라이언트 요청');

  const filename = 'typewriter-1031024_1920.jpg';
  fs.readFile(filename, (err, data) => {
    res.writeHead(200, {'Content-Type' : 'image/jpeg'});
    res.write(data);
    res.end();
  });
});

server.on('close', () => {
  console.log('서버가 종료');
});

파일을 다뤄야해서 require() 메소드로 fs 모듈을 읽어들입니다. on 메소드를 호추라여 request 이벤트를 처리하고, 클라이언트의 요청이 들어오면 readFile() 메소드로 jpg 파일을 읽어들입니다. readfile() 메소드는 비동기 방식으로 처리됩니다. 따라서 파일을 모두 읽으면 콜백 함수 안의 data 객체의 write() 메소드를 사용해서 파일 내용을 클라이언트로 전송합니다. 그리고 HTTP 헤더 중에서 Content-Type 헤더값으로 image/jpeg를 넣어 이미지 데이터임을 표시합니다. 

 

 

Content-Type에 설정할 수 있는 대표적인 MIME Type

MIME Type ? Multipurpose Internet Mail Extensions의 약어. 메시지의 내용이 어떤 형식인지 알려주기 위해 정의한 인터넷 표준입니다. 

text/plain : 일반 텍스트 문서

text/html : HTML 문서

text/css : CSS 문서

text/xml ㅣ XML 문서

image/jpeg, image /png : JPEG, PNG 파일

video/mpeg, audio/mp3 : MPEG 비디오 파일, MP3 음악 팔일

application/zip : ZIP 압축파일

 

 

파일을 스트림으로 읽어 응답 보내기

더 간단하게 만들 수 있는 방법이 파이프입니다. 파일은 스트림 객체로 읽어 들일 수 있고 웹 서버의 응답 객체도 스트림으로 데이터를 전송할 수 있기 때문에 두 개의 스트림은 파이프로 서로 연결할 수 있습니다. 

const http = require('http');
const fs = require('fs');

const server = http.createServer();

const port = 3000;
server.listen(port, () => {
  console.log('Server Running! : %d', port);
});

server.on('connection', (socket) => {
  const addr = socket.address();
  console.log('클라이언트 접속 : %s, %d', addr.address, addr.port);
});

server.on('request', (req, res) => {
  console.log('클라이언트 요청');

  const filename = 'typewriter-1031024_1920.jpg';
  const infile = fs.createReadStream(filename, {flag:'r'});
  // 파이프로 연결
  infile.pipe(res);

});

server.on('close', () => {
  console.log('서버가 종료');
});

앞의 예제와 같은 결과를 볼수 있습니다. 적은 양의 코드로 만들었지만 헤더를 설정할 수 없는 등의 제약이 생기므로 필요할때만 사용하길 권합니다.

 

 

 

파일을 버퍼에 담아 두고 일부부만 읽어 응답 보내

파일을 버퍼에 담아 일정 크기만큼 읽어 응답을 보내는 방법입니다.  스트림에서 파일을 읽을 때 버퍼의 크기르 정하면 파일을 버퍼 크기만큼씩 읽어 응답할 수 있습니다. 그런 다음 마지막에 end() 메소드를 호출하여 응답으로 전송할 수 있습니다.

const http = require('http');
const fs = require('fs');

const server = http.createServer();

const port = 3000;
server.listen(port, () => {
  console.log('Server Running! : %d', port);
});

server.on('connection', (socket) => {
  const addr = socket.address();
  console.log('클라이언트 접속 : %s, %d', addr.address, addr.port);
});

server.on('request', (req, res) => {
  console.log('클라이언트 요청');

  const filename = 'typewriter-1031024_1920.jpg';
  const infile = fs.createReadStream(filename, {flag:'r'});
  let filelength = 0;
  let curlength = 0;

  fs.stat(filename, (err, stats) => {
    filelength = stats.size;
  });

  //헤더쓰기
  res.writeHead(200, {'Content-Type' : 'image/jpeg'});

  //파일 내용을 스트림에서 읽어 본문쓰기
  infile.on('readable', () => {
    let chunk;
    while (null !== (chunk = infile.read())) {
      console.log('읽어 들인 데이터 크기 : %d 바이트 ', chunk.length);
      curlength += chunk.length;
      res.write(chunk, 'utf8', (err) => {
        console.log('파일 부분 쓰기 완료 : %d, 파일크기 : %d ', curlength, filelength);
        if (curlength >= filelength) {
          //응답 전송
          res.end();
        }
      });
    }
  });
});

server.on('close', () => {
  console.log('서버가 종료');
});

코드양이 조금 많으진 이유는 파일을 읽기 전에 먼저 파일을 열고, 그 다음에 스트림에서 일정 크기 만큼만 읽어 응답을 보내는 방식이기 때문입니다.

 

응답 객체의 end() 메소드를 호출하는 시점은 write() 메소드가 종료되는 시점이어야 합니다. 따라서 write() 메소드에 콜백 함수를 전달하여 쓰기가 완료된 시점을 확인합니다.  그 콜백함수 안에서는 파일의 크기만큼 응답 객체에 쓰기를 모두 완료했는지 확인하고, 응답 객체에 쓰는 작업이 모두 완료 되었다면 end() 메소드를 호출하여 응답데이터를 전송합니다. 

 

 

서버에서 다른 웹 사이트의 데이터를 가져와 응답하기

http 모듈은 서버 기능 이외에도 클라이언트 기능도 제공합니다. 즉, HTTP 클라이언트가 GET방식과 POST 방식으로 다른 웹 서버에 데이터를 요청할 수 있습니다.

const http = require('http');

const options = {
  host:'www.google.com',
  port:80,
  path:'/'
};

const req = http.get(options, (res) => {
  //응답 처리
  let resData = '';
  res.on('data', (chunk) => {
    resData += chunk;
  });
  res.on('end', () => {
    console.log(resData);
  });
});

req.on('error', (err) => {
  console.log('오류발생 : ', err.message);
});

get() 메소드의 첫 번째 파라미터는 다른 사이트의 정보를 담고 있는 객체입니다. 그리고 두 번째 파라미터는 콜백 함수 입니다. 응답 데이터를 받을 때는 data 이벤트와 end 이벤트로 처리합니다. 데이터를 받고 있는 상태에서는 data 이벤트가 발생하므로 이때 받은 데이터를 모두 resData 변수에 담아 둡니다.

 

end 이벤트를 처리하면 응답 데이터를 모두 받은 후에 콘솔 창에 출력할 수 있습니다. 이 파일을 실행하면 다음  처럼 구글 사이트에서 받아 온 응답을 볼 수 있습니다.

다른 서버로부터 응답을 받을 때는 data 이벤트가 발생합니다. 수신 데이터의 용량에 따라 data 이벤트는 한 번 또는 여러 번 발생할 수 있습니다. 데이터 수신이 완료되면 end 이벤트가 발생합니다.

 

 

const http = require('http');

const opts = {
  host:'www.google.com',
  port:80,
  method:'POST',
  path:'/',
  headers:{}
};

let resData = '';
const req = http.request(opts, (res) => {
  //응답 처리
  res.on('data', (chunk) => {
    resData += chunk;
  });
  res.on('end', () => {
    console.log(resData);
  });
});

opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
req.data = 'q=actor';
opts.headers['Content-Length'] = req.data.length;

req.on('error', (err) => {
  console.log('오류발생 : ', err.message);
});

//요청 전송
req.write(req.data);
req.end();

POST 방식으로 요청할 경우에는 request() 메소드를 사용합니다. 이 request()메소드는 get() 메소드ㅘ 사용 방식이 약간 다릅니다. 특히 요청을 보내려면 요청 헤더와 본문을 모두 직접 설정해야 합니다. 

 

구글 사이트에 요청할 때 필요한 요청 파라미터는 요청 객체의 data 속성으로 설정ㅇ합니다. 이 속성 값에 따라 Content-Length 헤더의 값이 달라지므로 request() 메소드로 만든 요청 객체에 이 정보를 추가로 설정합니다. 요청할 때는 write()메소드로 요청 본문 데이터를 req 객체에 쓴 후 end() 메소드를 사용해 전송합니다. 이 파일을 실행하면 구글 사이트에서는 POST 요청을 받지 못하므로 요류가 표시됩니다. 하지만 POST 메소드로 요청할 수 있는 다른 사이트에서는 정상적인 응답을 받을 수 있습니다.

 

 

참고한 책 : Do it Node.js

728x90
300x250