[과제] mini node server

과제 개요

라이브러리 사용하지 않고 순수 node.js로 서버 만들기! 

 

목표는 입력한 문자를 누르는 버튼에 따라 대문자 또는 소문자로 반환해주는 서버를 완성하는 것이다.

client부분은 이미 완성되어있고, server부분의 나머지를 완성해야 한다. 

 

  • POST에 문자열을 담아 요청을 보낼 때는 HTTP 메시지의 body(payload)를 이용합니다.
  • 서버는 요청에 따른 적절한 응답을 클라이언트로 보내야 합니다.
  • CORS 관련 헤더를 OPTIONS 응답에 적용해야 합니다.
    • 클라이언트의 preflight request에 대한 응답을 돌려줘야 합니다.
    • preflight request에 대한 응답 헤더는 이미 작성되어 있습니다.

 

과제를 완성하기 위해서는 node.js로 서버 만드는 법을 알아야 하는데, 이를 위해서 node 공식 사이트의 HTTP 트랜잭션 해부 부분을 공부해야 한다.

사실 나같은 초보가 공식 문서를 이해하기란 쉬운 일이 아니라 과제를 하면서 굉장한 골머리를 앓았다. 그러다 2년쯤 전에 배웠던 네트워크 운용 중, http에서 전송되는 데이터는 헤더와 바디를 가진다는 사실이 떠올라 이 한 문장으로 http트랜잭션이 조금은 이해가 되었다. 

 

이제 HTTP 트랜잭션 해부에서 Node.js HTTP 처리 과정에 대해 내가 이해한 부분을 조금 정리해봐야겠다.

아래 예시 코드는 HTTP 트랜잭션 해부 에서 가져온 것이다. 

 

1. 서버 생성 

우선 서버를 생성해야 한다. createServer를 통해 서버 객체를 생성할 수 있다. 이렇게 생성된 server는 EventEmitter 라는데, 찾아보니 EventEmitter는  

특정 이벤트에 리스너 함수를 달아서, 이벤트가 발생했을 때 이를 캐치할 수 있도록 만들어진 api
(출처: EventEmitter란 ?)

라고 한다. 그래서 http요청이 들오는 이벤트가 발생할 때 마다 server가 실행되는 것이다. 

const http = require('http');

const server = http.createServer((request, response) => {
  // 여기서 작업이 진행됩니다!
});

 

2-1. request header : method, url

전송받은 http 요청의 request header를 보면 method와 url을 통해 무엇을 원하는지를 알 수 있다.

 

method: 수행할 작업(GET, PUT, POST 등)이나 방식(HEAD or OPTIONS)
url: 전체 URL에서 서버, 프로토콜, 포트를 제외한 것으로, 세 번째 슬래시 이후의 나머지

method 출처: 코드스테이츠 자료
url 출처: HTTP 트랜잭션 해부

 

내가 한 과제의 경우 method가 OPTIONS면 프리플라이트 요청(리소스 접근 권한 확인 요청)을, POST면 응답을 요청했는데, 다만 method가 POST이면서 url이 /upper인 경우 받은 데이터를 대문자로 변환한 값을, method가 POST이면서 url이 /lower인 경우 소문자로 변환한 값을 반환할 것을 요구했다. 

 

method와 url은 creatServer 에서 request로 받아오기 때문에 creatServer안에서 request.method, request.url 등으로 쉽게 사용할 수 있다.

 

2-2. request body

let body = [];
request.on('data', (chunk) => {
  body.push(chunk);
}).on('end', () => {
  body = Buffer.concat(body).toString();
  // 여기서 `body`에 전체 요청 바디가 문자열로 담겨있습니다.
});

HTTP 트랜잭션 해부에 따르면 request 객체는 readableStream 인터페이스를 구현한다고 한다.

Node.js는 데이터를 클라이언트에 전달하거나, 클라이언트로부터 데이터를 받을 때, 또는 파일을 읽고 쓸 때 Readable stream과 Writable stream을 사용하게 된다.
(출처: Readable Stream을 다루는 방법)

 

스트림에 이벤트리스너를 등록하거나 다른 스트림과 파이핑을 할 수 있는데, 과제에 필요한 것은 이벤트리스너의 등록이다. 

 

.on('data')

data를 등록하면 readableStream이 알아서 전송받은 데이터를 가져온다.

위 예제는 콜백함수를 이용해 이 데이터를 chunk라는 매개변수로 받아 body라는 배열에 push한다. 이 때 받아오는 데이터 chuck는 문자열이고, 이 데이터 조각들을 body 배열에 담아 .on('end') 이벤트리스너에서 이어붙인 후 다시 문자열로 반환하는 것이다. end까지 돌면 최종 데이터는 문자열이 되는 것이다.

청크는 스트림에 쓰거나 스트림에서 읽는 단일 데이터 조각입니다. 
(출처: Streams—최종 가이드)

.on('end')

end는 스트림을 종료하고, 최종 내용을 클라이언트로 전송한다.

위 예제에서는 .on('end') 이벤트리스너에 배열에 담은 chunk를 이어붙인 후 다시 문자열로 반환하는 최종 수식을 작성하였다.

 

3-1. response header : status

* response header form
: 웹브라우저가 요청한 메시지에 대해서 status 즉 성공했는지 여부(202, 400 등), 메시지, 그리고 요청한 응답 값들이 body에 담겨있다.
(출처: HTTP의 구조에 대해 이해하기(특히 Header 구조 파악하기))

다시 클라이언트로 반환하는 데이터 또한 http이므로 헤더와 본문으로 구성되어있다. 헤더를 명시적으로 작성하기 위해서는 writeHead 메서드를 사용할 수 있다. 첫 번째 인자에는 상태코드를, 두 번째 인자에는 헤더 정보를 객체 형식으로 담는다.

 

3-2. response body

response는 writableStream이다. 아래와 같이 일반적인 메서드나, end 메서드를 사용하여 처리할 수 있다.

response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();

//또는

response.end('<html><body><h1>Hello, World!</h1></body></html>');

 

일단 여기까지. 오류 처리 부분은 추후 더 공부하여 작성하겠다.

위 공부를 토대로 과제를 수행하였다. 

 

 

과제 - mini node server

//프리플라이트 요청 과 //본문 부분이 내가 짠 코드이다. 

 

const http = require('http');

const PORT = 4999;

const ip = 'localhost';

const server = http.createServer((request, response) => {
  //프리플라이트 요청
  if(request.method === 'OPTIONS'){
    response.writeHead(200, defaultCorsHeader); //헤더 하나
    response.end();                             //바디 하나 전송
    console.log('프리플라이트 성공');
  }

  //본문
  let body = [];
    request
    .on('error', (err) => {
      console.error(err);
    }).on('data', (chunk) => {
      body.push(chunk);
    }).on('end', () => {
      body = Buffer.concat(body).toString();  
      // 전송받은 데이터 body에 문자열로 담겨있음
      console.log(body)
      //upperCase요청
      if(request.method === 'POST' && request.url === '/upper'){
        response.writeHead(201, defaultCorsHeader); 
        response.end(body.toUpperCase());
      }
      //lowerCase요청 
      else if(request.method === 'POST' && request.url === '/lower'){
        response.writeHead(201, defaultCorsHeader); 
        response.end(body.toLowerCase());
      } 
      //에러처리
      else{
        response.statusCode = 404;
        response.end();
      }
    });
  

  console.log(
    `http request method is ${request.method}, url is ${request.url}`
  );
  // response.writeHead(200, defaultCorsHeader);
  // response.end('hello mini-server sprints');
});

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

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
};

 

 

 

과제는 해결했지만 제대로 이해하고 있다는 생각이 들지 않아 이렇게 글로 적어보았다. 확실히 글로 적는동안 여러 자료를 찾아보며 내가 한게 무엇인지 조금 더 정확하게 인지하게 되었다! 시간이 많지 않아 아쉽게도 공부할 부분을 남겨놓게 되었지만 한 번에 완벽하게 하기보단 조금씩 꾸준히 해나가는것이 좋을 것이다. 계속 화이팅.

 

더 공부하면 좋을 부분

  • Readable stream 과 Writable stream의 파이핑
  • 서버의 오류 처리