[Node.js] 2. 서버 연습

rin·2020년 12월 13일
0
post-thumbnail

https://poiemaweb.com/nodejs-file-upload-example

Basic HTTP server

  • HTTP server를 생성하는데에는 http 모듈을 사용한다.
  • http.createServer([requestListener])http.Server의 새로운 인스턴스를 반환한다.
    • requestListenerrequest 이벤트가 발생했을 때 자동으로 호출될 콜백 함수이다.
  • Server 인스턴스는 listen 메소드를 호출하여 접속 대기를 시작한다.
// server.js

// Node.js에 기본 내장되어 있는 http 모듈을 로드한다
var http = require("http");

// http 모듈의 createServer 메소드를 호출하여 HTTP 서버 생성
http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"}); // (1)
  response.write("Hello World");  // (2)
  response.end();                 // (3)
}).listen(8888);

Event-driven callbacks

  • Node.js는 event-driven, non-blocking I/O model을 지원한다.
  • Javascript의 함수는 일급객체이므로 변수와 같이 사용할 수 있으며 코드의 어디에서든지 정의할 수 있다.
var http = require("http");


const onRequest = (request, response) => {
    console.log("request received.");
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("hello world!");
    response.end();
};

http.createServer(onRequest).listen(8888);

모듈화

  • 모듈화는 모듈을 필요로 하는 스크립트에 제공할 기능의 일부를 export 하는 것이다.
  • HTTP 서버 생성 로직을 함수에 담아 export한다.
// server.js
var http = require("http");

const start = () => {
    const onRequest = (request, response) => {
        console.log("request received.");
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write("hello world!");
        response.end();
    };
    
    http.createServer(onRequest).listen(8888);

    console.log("Server has started");
}

exports.start = start;


// index.js
var server = require("./server");

server.start();

Routing

  • 요청 URL과 GET/POST 파라미터를 router로 전달하면 router는 어떤 코드를 실행할지 결정할 수 있어야한다.
  • 이 때 서버의 할 일을 수행하는 함수를 request handler라 한다.
// url & querystring modules

                        url.parse(string).query
                                        |
          url.parse(string).pathname    |
                        |               |
                        |               |
                      ----- -------------------
http://localhost:8888/start?foo=bar&hello=world
                                ---       -----
                                 |          |
                                 |          |
        querystring.parse(string)["foo"]    |
                                            |
                querystring.parse(string)["hello"]
  • 필요한 정보는 request 객체를 통해 접근할 수 있다.
  • url 모듈 : URL의 각 부분을 추출할 수 있는 메소드를 제공
  • querystring 모듈 : query string을 request 파라미터로 파싱, POST 요청의 body를 파싱하는 데 사용
var http = require("http");
var url = require("url");

const start = () => {
    const onRequest = (request, response) => {
        var pathname = url.parse(request.url).pathname;
        console.log("Path name is " + pathname);

        var query = url.parse(request.url, true).query;
        console.log("Request param is : ", query);
        
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write("<h1>Path name is "+ pathname + "</h1>" +
                        "<h1>Request param is "+ JSON.stringify(query) + "</h1>");
        response.end();
    };
    
    http.createServer(onRequest).listen(8888);

    console.log("Server has started");
}


exports.start = start;

  • url 모듈을 사용하여 URL path를 기준으로 요청을 구분할 수 있다.
// router.js
const route = pathname => {
    console.log("About to route a request for "+ pathname);
};

exports.route = route;

// server.js
var http = require("http");
var url = require("url");

const start = route => {
    const onRequest = (request, response) => {
        var pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");

        route(pathname);

        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write("Hello World");
        response.end();
    };
    
    http.createServer(onRequest).listen(8888);

    console.log("Server has started");
}

exports.start = start;

// index.js
var server = require("./server");
var router = require("./router");

server.start(router.route);
  • router.js : path를 파라미터로 받는다.
  • server.js : route 함수를 파라미터로 받아서 url 모듈로 추출한 값을 해당 함수에 넘겨준다.
  • index.js : router 모듈의 route 함수를 server.start의 파라미터로 전달한다.

Request Handler

  • Request handler를 만들어 path에 따라 핸들러 내 다른 함수를 호출할 수 있도록 해보자.
  • 실제 클라이언트에 응답 컨텐츠를 반환하기위해 return 값을 넣는다.
// requestHandler.js
exports.start = () => {
    console.log("request handler 'start' was called");
    return "Hello Start";
}

exports.upload = () => {
    console.log("request handler 'upload' was called");
    return "Hello Upload"
}
  • index 에서는 위에서 만든 핸들러 함수를 Object(key:value)로 만들어준다.
// index.js
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");

var handle = {};
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;

server.start(router.route, handle);
  • pathName을 받는 것은 route이고, 어떤 path인지 판단해서 적절한 처리를 하는 것이 route의 업무이다.
  • server.start는 route에 handle 오브젝트와 pathname을 전달한다.
// server.js
var http = require("http");
var url = require("url");

const start = (route, handle) => {
    const onRequest = (request, response) => {
        var pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");

        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write(route(handle, pathname));
        response.end();
    };
    
    http.createServer(onRequest).listen(8888);

    console.log("Server has started");
}

exports.start = start;

// router.js
const route = (handle, pathname) => {
    console.log("About to route a request for "+ pathname);
    if (typeof handle[pathname] === 'function') {
        return handle[pathname]();
    } else {
        console.log("No request handler found for "+ pathname);
        return "404 NOT FOUND"
    }
};

exports.route = route;

Blocking vs. Non-Blocking

사실 위의 코드는 requestHandler에 비동기 방식의 코드를 포함시키면 문제가 발생한다.

Node.js는 이벤트 기반 비동기 방식으로 작동한다.

  • 동시작업을 event loop를 실행해서 처리
  • 단일 쓰레드

동기방식 코드를 비동기로 전환하는 방법에는 다음과 같은 두가지가 있다.

  1. 동기방식 API에 대응하는 코드를 비동기 API로 교체한다.
  2. 동기 방식에서 동기 API 호출 이후에 순차적으로 처리되어야하는 로직을 그대로 콜백 함수로 옮긴다.

requestHandler에 비동기 방식의 코드를 포함시켜보자.

const { exec } = require("child_process");
const { stderr } = require("process");

exports.start = () => {
    console.log("request handler 'start' was called");

    var content = "empty";
    exec("ls -lah", (error, stdout, stderr) => {
        content = stdout;
    })

    return content;
}

exports.upload = () => {
    console.log("request handler 'upload' was called");
    return "Hello Upload"
}
  • exec은 non-blocking 방식으로 동작한다.
  • 아래와 같이 현재 디렉토리의 모든 파일 리스트가 아닌 empty를 표시한다.
  • 즉, exec을 호출한 후 결과를 기다리지 않고 바로 return content;를 실행하는 것이다.

Non-blocking 방식 request handler

지금까지 content는 다음과 같이 이동하였다.

  • requestHandler => router => server

새로운 방법에서는 이동 방향을 역으로 전환할 것이다.
즉, http.createServer의 callback인 onRequest()에서 얻은 response 객체를 router를 통해 requestHandler에게 주입한다.

  • route 함수에 response 객체를 통채로 전달한다.
// server.js
var http = require("http");
var url = require("url");

exports.start = (route, handle) => {
    const onRequest = (request, response) => {
        var pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");

        route(handle, pathname, response);
    };
    http.createServer(onRequest).listen(8888);
    console.log("Server has started");
}
  • route 함수는 전달받은 response를 requestHandler의 각 함수에 그대로 전달한다.
  • requestHandler의 start 함수는 비동기 요청이 종료되면 response 객체를 이용하여 클라이언트에 응답한다.
// router.js
exports.route = (handle, pathname, response) => {
    console.log("About to route a request for "+ pathname);
    if (typeof handle[pathname] === 'function') {
        return handle[pathname](response);
    } else {
        console.log("No request handler found for "+ pathname);
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write("404 NOT FOUND");
        response.end();
    }
};

// requestHandlers.js
const { exec } = require("child_process");
const { stderr } = require("process");

exports.start = response => {
    console.log("request handler 'start' was called");

    exec("ls -lah", (error, stdout, stderr) => {
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write(stdout);
        response.end();
    })
}

exports.upload = response => {
    console.log("request handler 'upload' was called");
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("Hello Upload");
    response.end();
}

Handling POST requests

  • textarea 필드로 입력받은 값을 form 태그의 submit을 이용해 뷰에 보여주도록 할 것이다.
  • Non-blocking 방식으로 만들기 위해서는 POST 데이터를 작은 청크로 나눠서 처리해야한다.
    • 전체 데이터 블록을 하나로 처리하는 것은 blocking 방식이다.
  • request 객체에 이벤트 리스너를 추가함으로써 이벤트의 시작과 끝을 핸들링할 수 있다.
    • request.addListener(eventName, callback)
    • data 이벤트 : POST 데이터의 새 청크가 도착했다.
    • end 이벤트 : 모든 청크를 다 받았다.
  • request.addListener 대신 request.on도 가능하다.
// server.js
var http = require("http");
var url = require("url");

exports.start = (route, handle) => {
    const onRequest = (request, response) => {
        var postData = "";
        var pathname = url.parse(request.url).pathname;
        console.log("Request for " + pathname + " received.");

        request.setEncoding("utf8");
        request.addListener("data", postDataChunk => {
            postData += postDataChunk;
            console.log("Received POST data chunk '"+ postDataChunk +"'.")
        })
        request.addListener("end", () => {
            route(handle, pathname, response, postData);
        })        
    };
    http.createServer(onRequest).listen(8888);
    console.log("Server has started");
}

// router.js
exports.route = (handle, pathname, response, postData) => {
    console.log("About to route a request for "+ pathname);
    if (typeof handle[pathname] === 'function') {
        return handle[pathname](response, postData);
    } else {
        console.log("No request handler found for "+ pathname);
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write("404 NOT FOUND");
        response.end();
    }
};

// requestHandler.js
const { exec } = require("child_process");
const { stderr } = require("process");

exports.start = response => {
    console.log("request handler 'start' was called");

    var body = '<html>'+
    '<head>'+
    '<meta http-equiv="Content-Type" content="text/html; '+
    'charset=UTF-8" />'+
    '</head>'+
    '<body>'+
    '<form action="/upload" method="post">'+
    '<textarea name="text" rows="20" cols="60"></textarea>'+
    '<input type="submit" value="Submit text" />'+
    '</form>'+
    '</body>'+
    '</html>';

    exec("ls -lah", (error, stdout, stderr) => {
        response.writeHead(200, {"Content-Type": "text/html"});
        response.write(body);
        response.end();
    })
}

exports.upload = (response, postData) => {
    console.log("request handler 'upload' was called");
    response.writeHead(200, {"Content-Type": "text/plain"});
    response.write("You've sent: "+postData);
    response.end();
}
  • 전달받은 postData를 사용하는 함수는 upload 이다.
  • start 함수와 같이 Controller와 View를 하나의 모듈 내에 구현하는 것은 사실 옳지않다.
profile
🌱 😈💻 🌱

0개의 댓글