Nextree

Node.js: Hello로 시작하는 Web 애플리케이션

Nextree Mar 28, 2014 0 Comments

2009년 Ryan Dahl이 발표한 Node.js 는 자바스크립트로 서버 애플리케이션을 구현할 수 있게 해 주는 서버 플랫폼입니다. C나 Java 언어가 주도하던 기존의 기업형 서버 환경에서 이제 자바스크립트가 하나의 영역으로 자리 잡아가고 있습니다. Node.js 덕분에 자바스크립트를 잘 구사하는 UI 개발자들도 서버 영역에 진입할 수 있게 된 것이죠.

그러나 자바스크립트 언어만 능숙하다고 당장 Node 애플리케이션을 구현할 수는 없습니다. Node.js 개발자는 이벤트 기반의 비동기 프로그램을 이해해야 하며 Node.js 및 주변 인프라에서 제공하는 모듈 및 미들웨어를 잘 알아야 제대로 구현할 수 있습니다. 이 글은 Node.js로 어디서 부터 무엇을 해야 하는지 잘 모르는 사람들을 위한 가이드를 제공합니다.

시나리오는 Hello로 부터 시작하여 실제 기업형 애플리케이션과 유사한 형태로 발전시켜 나갈 예정입니다. 이 과정의 첫머리에는 당장 express와 같은 프레임워크를 사용하지 않고 순수 Node 기능만 사용하여 기본기에 충실하도록 하였습니다.

Hello World

Node.js 애플리케이션은 자바스크립트 언어로 구현하며 .js 파일 형태로 존재합니다. 이러한 .js 파일은 Node로 실행이 가능합니다. 당장 문서 편집기를 열고 다음과 같이 코드를 작성합니다.

console.log('Hello nodejs');  

작성된 파일을 hello.js 로 저장하고 Node로 실행시켜 봅니다.

d:\nodejs>node hello.js
hello nodejs

콘솔에 Hello 로그를 출력하고 바로 종료합니다. 이와같이 Node.js 엔진은 스크립트 파일을 수행 후 할 일이 없으면 바로 종료합니다. 그러나 우리는 이 예제로 서버라고 말하기가 어렵습니다. 따라서 다음과 같이 간단한 시나리오를 생각해 보겠습니다.

간단한 시나리오

사용자는 조회를 위한 요청을 보내면 서버는 그 결과를 응답한다.
사용자는 데이터 생성이나 저장을 위한 요청을 보내면 서버는 데이터를 받은 후 처리한다.
모든 요청이나 응답은 HTTP 프로토콜을 사용한다.

간단한 시나리오

시나리오를 보면 사용자가 어떤 내용을 조회하고 저장하는 가장 기초적인 내용임을 알 수 있습니다. 우리는 이 시나리오를 구현하기 위하여 HTTP 통신을 할 수 있는 서버를 구현해야 합니다. Node.js는 서버를 구현하기 위한 간단한 방법을 제공하고 있습니다.

다음 예제와 같이 server.js 파일을 만들어 봅니다.

var http = require('http');

http.createServer(function (request, response) {  
    response.writeHead(200, {'Content-Type' : 'text/plain'});
    response.write('Hello nodejs');
    response.end();
}).listen(8888);

파일을 저장하고 이전 예제와 같이 Node로 파일을 실행시켜 봅니다. 이번에는 서버가 바로 종료하지 않습니다. 그리고 웹 브라우저를 열고 localhost:8888 을 입력해 봅니다. 그러면 웹 브라우저 화면에 "Hello nodejs" 메시지가 출력됩니다.
서버를 종료하려면 Ctrl+C 를 하면 됩니다.

이것이 가장 간단한 형태의 http 서버입니다. http 서버를 만들기 위하여 Node.js 가 내장하고 있는 'http' 모듈을 사용하였습니다. 기본적으로 모듈을 사용하려면 require() 전역함수를 이용하여 모듈을 로드하여야 합니다. Node.js 내장모듈을 사용하는 방법은 http://nodejs.org/api/ 에 설명이 되어 있으므로 참고하시기 바랍니다.

Http 서버 인스턴스 생성은 http 모듈의 createServer() 함수를 사용합니다. 그리고 서버 인스턴스의 listen() 함수는 http 서버를 시작하게 하며 여기서 사용자의 요청을 받도록 대기합니다. 이 서버는 클라이언트의 요청에 응답하기 위하여 Callback 함수가 등록되었고 response 객체로 응답을 하게 됩니다.

자, 그럼 여기서 어떤 사람은 Callback이 무엇이고 함수를 등록하였다는 것이 무엇인지 잘 이해가 가지 않을 겁니다. 그럼 Callback 과 함수전달 개념에 대해 좀 더 알아보겠습니다.

Http Server 동작 원리

Http Server 는 리스너 기반으로 동작합니다. 리스너가 무엇일까요? 가령 다음 예제와 같은 서버도 생각해 볼 수 있습니다.

var http = require('http');

var server = http.createServer();  
server.listen(8888);  

이 서버는 인스턴스를 생성하고 시작만 하였을 뿐이지 클라이언트의 어떤 요청에 대해서도 응답을 하지 않습니다. 따라서 이 서버를 실행 시킨 후 웹브라우저에서 요청을 보내도 아무런 응답이 없습니다.

그렇습니다. Http Server는 서버 인스턴스 생성시 리스너가 등록되어야 요청 처리를 할 수 있는 것입니다. 이러한 리스너는 함수 형태로 존재합니다. 우리는 요청을 처리 할 수 있는 함수를 정의해야 합니다. 함수 정의는 익명 또는 기명으로 할 수 있습니다. 앞에서 본 server.js 예제는 익명 함수를 정의한 예입니다.

우리는 이 코드를 간단하게 기명 함수로 변경할 수 있습니다.

var http = require('http');

function onRequest(request, response) {  
    response.writeHead(200, {'Content-Type' : 'text/plain'});
    response.write('Hello World');
    response.end();
}

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

기능은 이전 방식과 동일합니다. 그러나 이번 방식이 기명 함수이므로 코드가 좀 더 명확합니다. 그리고 서버 인스턴스 생성시 이 함수를 전달하는 방식으로 리스너를 등록합니다.

리스너 등록은 위의 예제와는 다르게 등록할 수도 있습니다. 그것은 addListener() 를 이용하는 방법입니다.

var http = require('http');

var server = http.createServer();

server.addListener('request', function (request, response) {  
    console.log('requested...');
    response.writeHead(200, {'Content-Type' : 'text/plain'});
    response.write('Hello nodejs');
    response.end();
});

server.addListener('connection', function(socket){  
    console.log('connected...');
});

server.listen(8888);  

기능은 처음 예제와 동일합니다. 다만 이 방법을 사용하면 좀 더 다양한 이벤트를 처리할 수 있습니다. 즉, 요청 이벤트인 'request' 와 클라이언트 접속 이벤트인 'connection' 를 따로 처리할 수 있습니다. 이러한 방법으로 이벤트 리스너를 등록하려면 server가 어떤 종류의 이벤트 타입이 있는지 미리 알아야 합니다. 이럴 경우 Node.js http API 문서를 확인하면 됩니다.

비동기 Callback 호출

Node.js는 서버에서 발생한 모든 이벤트를 비동기로 처리합니다. IO 처리에 대한 Blocking을 방지하기 위함이죠. 이벤트를 비동기로 처리하면 이 작업이 언제 완료되는지 알 수 없습니다. 따라서 이벤트 처리와 동시에 처리 완료에 대한 정보를 Callback으로 전달 해 주는 것입니다. 비동기 처리에 대한 좀 더 자세한 내용은 비동기 프로그래밍 이해 글을 참조하세요.

비동기 Callback 호출

비동기 Callback 호출 위의 예제에서 onRequest() 함수를 이벤트 리스너로 등록하였습니다. 이 함수는 'request' 이벤트에 대한 Callback 역할을 합니다. 클라이언트가 Http 요청을 보내면 Http Server에 'request' 타입 이벤트가 발생하고 이 이벤트는 비동기로 처리가 됩니다. 처리가 완료되면 onRequest() 함수가 호출되는 것입니다.

우리는 다음과 같이 server.js 파일 안에 로그 출력을 위한 코드를 넣을 수 있습니다.

var http = require('http');

function 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.');  

server.js 를 다시 실행하면 'server has started' 메시지가 먼저 출력됩니다. 'request received' 메시지는 Callback 함수 내에 있으므로 클라이언트의 요청이 있을 때만 출력이 됩니다.

한가지 재미있는 사실은 http://localhost:8888 로 호출시 메시지 로그가 두 번 출력된다는 사실입니다. 그 이유는 요즘 나오는 대부분의 브라우저가 favicon.ico 를 위한 요청을 한번 더 보내기 때문입니다.

단위 JS 모듈화

우리는 앞으로 server.js 를 확장하여 더 많은 기능들을 추가해야 합니다. 그러면 server.js 파일은 점점 복잡해지고 덩치가 커질거라는 점을 예상할 수 있습니다. 간단한 프로그램이면 상관 없겠지만 여러명의 개발자들이 대규모로 개발한다면 문제가 되겠지요? 그래서 애플리케이션 기능들을 단위 모듈로 나누어 설계와 개발을 합니다.

Node.js 는 이러한 모듈 기능을 제공합니다. Node.js 모듈은 CommonJS의 모듈 개념을 구현하였으며 독자적인 Context를 가집니다. 따라서 Global 스코프를 오염시키지 않습니다.

우리는 다음 예제와 같이 server.js 를 수정하여 모듈로 만들 수 있습니다.

var http = require('http');

function start() {  
    function 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;  

그리고 server.js 모듈을 사용하는 index.js 도 만들어 볼 겁니다.

var server = require('./server');

server.start();  

모듈 정의와 사용

server.js 에서 서버를 실행하는 start() 함수를 만들어 지금까지 작성한 코드를 함수 내로 옮깁니다. 그리고 Global 변수인 exports를 사용하여 start() 함수를 모듈로 등록합니다.

index.js는 server.js 모듈을 로드합니다. require() 전역 함수를 사용하면 .js 파일로 작성된 모듈을 로드할 수 있습니다. index.js는 server.js의 start() 함수를 호출하여 서버를 시작할 수 있습니다.

이와 같은 모듈로 파일이 2개로 나뉘어졌습니다. 기능상 변한 것은 없지만 모듈화를 하면 단위 프로그램의 독립성은 확보할 수 있습니다.

의존성 주입(Dependency Injection)

Http Server는 다양한 사용자 요청에 응답해야 합니다. 예를 들면 조회를 위한 요청일 수 도 있고 저장을 위한 요청일 수도 있습니다. 이러한 요청들은 server.js가 수행하기에는 너무 범위가 넓습니다. 따라서 우리는 다양한 요청을 식별하는 router 모듈을 생각해 볼 수 있습니다.

다음과 같이 router.js 를 만듭니다.

function route(pathname) {  
    console.log('about to route a request for ' + pathname);
}

exports.route = route;  
의존성 주입

만들어진 router 모듈은 server.js 가 직접 로딩할 수도 있으나 server 동작의 유연성을 확보하기 위하여 server와 router와의 관계를 느슨하게 할 필요가 있습니다. 따라서 index.js에서 router 모듈을 로딩하고 route() 함수를 server에게 넘겨서 실행하는 방법을 사용하기로 합니다.

이러한 방법을 의존성 주입(Dependency Injection) 이라고 합니다. 이 방법의 장점은 관계가 느슨하므로 server 코드의 변경 없이 router 함수를 교체할 수 있다는 점입니다. server는 단지 전달된 router를 실행하기만 할 뿐 router가 무엇인지 관심이 없습니다.

따라서 우리는 router.js 모듈을 index.js에서 로드하고 route() 함수를 server로 넘깁니다.

var server = require('./server');  
var router = require('./router');

server.start(router.route);  

server.js 는 start() 실행시 route 함수를 넘겨 받으며 'request' 이벤트가 발생할 때 마다 route 함수를 호출하여 요청을 식별하도록 합니다.

var http = require('http');  
var url = require('url');

function start(route) {  
    function onRequest(request, response) {
        var pathname = url.parse(request.url).pathname;
        console.log('request for ' + pathname + ' received.');

        route(pathname); // injected function call

        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;  

server.js에서는 'url' 모듈을 사용하여 요청 path를 추출한 뒤 route() 함수 호출시 path를 넘겨줍니다.

요청 처리자(Request Handler) 식별

지금까지 식별된 모듈은 server 와 router입니다. server는 클라이언트의 요청을 받고 router는 다양한 요청을 처리해야 합니다. 이러한 상황에서 역할 분리가 필요할 것 같습니다.

요청 처리자 식별

router는 주어진 path에 따라 요청을 분배하기만 하고 실제 요청 처리는 requestHandler가 담당합니다. 즉 요청 식별과 처리 모듈을 분리하는 것이죠. 요청을 처리하는 requestHandler 함수는 요청 path에 따라 다양하게 존재할 수 있습니다.

따라서 다음과 같이 다양한 requestHander 함수들을 가지고 있는 requestHandlers.js 파일을 모듈로 작성합니다.

function view() {  
    console.log('request handler called --> view');
}

function create() {  
    console.log('request handler called --> create');
}

var handle = {}; // javascript object has key:value pair.  
handle['/'] = view;  
handle['/view'] = view;  
handle['/create'] = create;

exports.handle = handle;  

이 모듈의 특징은 다양한 요청을 처리해야 하는 함수들을 정의한 점입니다. 서두에서 소개한 시나리오와 같이 조회를 위한 요청은 view() 함수에서 처리하며 저장을 위한 요청은 create() 함수에서 처리합니다. 만일 또 다른 요청 유형이 식별되면 처리 함수를 하나 더 만들면 됩니다.

이 모듈의 또 다른 특징은 함수를 export 하지 않고 자바스크립트 객체를 export 하였다는 점입니다. router 에서는 수많은 요청 path 에 따라 적절한 handler를 결정하여야 하는데 이는 수많은 if... else 코드가 발생할 개연성을 내포하고 있습니다. 따라서 key:value 쌍으로 이루어진 객체를 제공하여 router가 handler를 쉽게 식별할 수 있게 합니다.

따라서 export 하는 handle 객체는 요청 path 를 key로 하고 처리 함수를 value로 가지고 있는 것입니다.

이제 만들어진 requestHandlers.js 모듈을 index.js에서 로드해 보겠습니다.

var server = require('./server');  
var router = require('./router');  
var requestHandlers = require('./requestHandlers');

server.start(router.route, requestHandlers.handle);  

requestHandlers.js 에서 제공하는 handle 객체를 start() 함수에 추가로 넘깁니다. server.js를 약간만 변경해 봅니다.

function start(route, handle) {  
    function onRequest(request, response) {
        ...
        route(handle, pathname);
        ...

router.js에서는 handle 객체를 받아 처리하는 로직이 필요합니다. router.js를 아래와 같이 수정합니다.

function route(handle, pathname) {  
    console.log('about to route a request for ' + pathname);
    if (typeof handle[pathname] === 'function') {
        handle[pathname]();
    } else {
        console.log('no request handler found for ' + pathname);
    }
}

exports.route = route;  

route() 함수는 넘겨받은 handle 객체에서 요청 path에 해당하는 함수를 꺼냅니다. 입력된 path에 해당하는 함수가 존재하면 그 함수를 실행합니다. 그러면 함수는 요청을 처리할 것입니다. 요청 path에 매핑되는 handler 함수가 없을 경우 '404 Not Found' 메세지를 router에서 보낼 예정입니다.

요청 처리 확인

요청 처리 확인 이제 index.js 로 서버를 실행시킨 후 다양한 요청을 보내봅니다. '/view' 요청을 보내면 정확하게 해당 handler 함수가 실행되는 것을 확인할 수 있습니다. 이는 서버 로그를 통해서 알 수 있습니다. 만일 '/abc'와 같은 요청을 보내면 해당 handler가 없으므로 handler가 없다는 메시지가 출력될 것입니다.

그러나 여전히 웹 브라우저에서 보이는 메시지는 'Hello World' 입니다. 이제 hander 함수가 응답할 수 있게 변경해 보겠습니다.

Response 처리

지금까지는 응답처리를 server.js에서 처리하도록 하였습니다. 이제 응답을 처리하는 handler 가 준비되었으므로 handler가 응답 처리할 수 있게 변경합니다. handler의 응답처리를 위해서는 response 객체를 handler에 넘겨주어야 합니다.

server.js 를 아래와 같이 변경합니다.

function start(route, handle) {  
    function onRequest(request, response) {
        var pathname = url.parse(request.url).pathname;
        console.log('request for ' + pathname + ' received.');

        route(handle, pathname, response);
    }
    ...

response로 응답처리하는 로직을 지우고 대신 response 객체를 route() 함수로 넘깁니다.

function route(handle, pathname, response) {  
    console.log('about to route a request for ' + pathname);
    if (typeof handle[pathname] === 'function') {
        handle[pathname](response);
    } else {
        console.log('no request handler found for ' + pathname);
        response.writeHead(404, {'Content-Type' : 'text/plain'});
        response.write('404 Not found');
        response.end();
    }
}
...

router.js 에서는 넘겨받은 response 객체를 다시 handle 함수에 넘깁니다. 적절한 handler가 없는 경우에는 여기서 직접 응답처리합니다.

function view(response) {  
    console.log('request handler called --> view');
    response.writeHead(200, {'Content-Type' : 'text/plain'});
    response.write("Hello View");
    response.end();
}

function create(response) {  
    console.log('request handler called --> create');
    response.writeHead(200, {'Content-Type' : 'text/plain'});
    response.write('Hello Create');
    response.end();
}
...

requestHandlers.js 에서 각 handler 함수를 변경합니다. response 파라메터를 추가하고 response 객체로 각 기능에 맞게 응답처리합니다.

Response 처리 확인

지금까지 작업한 각 js 파일들을 모두 저장하고 서버를 다시 시작합니다. '/view', '/create', '/abc' 처럼 각각 요청을 보내면 각 요청에 맞는 응답 메시지를 브라우저에서 확인 할 수 있습니다.

마치며

지금까지 가장 간단한 기본 시나리오를 가지고 'Hello' 부터 시작하여 어느정도 구조가 갖추어진 Web 애플리케이션을 만들어 보았습니다. 아직까지는 예제 시스템 수준이지만 Node.js 의 기본적인 기능은 어느정도 파악했으리라 봅니다.

이러한 시나리오 기반의 Node 애플리케이션은 여기서 그치는 것이 아니라 앞으로 나올 글에서 계속 발전시켜 나갈 예정입니다. 시나리오를 좀 더 확장하고 Node.js 의 다양한 모듈과 미들웨어를 사용할 예정이니 기대해 주시기 바랍니다.

관련글

  1. Node.js: 비동기 프로그래밍 이해
  2. Node.js: Hello로 시작하는 Web 애플리케이션

참고 문서 및 사이트

  1. [서적] The Node Beginner Book
  2. [서적] Professional Node.js: Building Javascript Based Scalable Software
  3. Understanding Node.js : http://debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb

Nextree

Read more posts by this author.