클라우드 컴퓨팅 & NoSQL/Vert.x & Node.js

빠르게 훝어 보는 node.js - #3 Event,Module,NPM

Terry Cho 2014. 3. 25. 01:13

빠르게 훝어보는 node.js

#3 - Event,Module,NPM

조대협 (http://bcho.tistory.com

비동기 이벤트 프로그래밍

기존의 프로그래밍 언어들은 일반적으로 함수를 부르는 형태의 프로그래밍 구조를 가지고 있다. 이를 procedural programming model이라고 하는데코드가 순차적으로 실행되면서 함수를 호출하는 식의 구조를 가지고 있기 때문에 코드를 보면 코드의 수행 순서를 예측할 수 있다.

 

node.js event driven programming 이라는 개념을 가지고 있는데, 이 개념은 특정 이벤트가 발생되면 미리 이벤트에 맵핑된 함수가 실행되는 형태이다. 즉 해당 함수가 언제 호출 되는지를 예측할 수 가 없다.

var callback = function(data){

        console.log("call back has been called "+data);

}

 

$.get('/endpoint',callback);

 

위의 코드에 있는 callback 이라는 함수는 HTTP GET /endpoint request가 발생할 때만 수행된다.

  cf. procedural programming의 경우 함수는 코드상에서 명시적으로 호출을 해줘야 발생하지만, event driven programming은 이벤트에 의해서 함수가 호출 된다.

이러한 이벤트 방식의 유사한 사례는 윈도우즈나, 자바 SWING과 같은 GUI 계통의 프로그래밍에서도 찾아볼 수 있다. Mouse Click이나 Button Click 같은 이벤트에, callback 함수를 Binding 시켜놓는 형태에서 볼 수 있다.

 

다음으로 node.js 특징은 비동기 프로그래밍 방식이 라는 것인데, 앞서 설명한 바와 같이 node.js는 비동기식 IO를 이용한다. IO 요청을 보내놓고, 코드를 blocking 상태에서 기다리는 것이 아니라 다음 코드로 진행한 다음, IO 가 끝났다는 이벤트가 오면, 미리 지정해놓은 함수를 실행하는 형태이다. 이렇게 함수를 호출한후, 작업이 끝난 후에, 호출되도록 정의한 함수를 “callback”함수라고 한다.

 

아래 코드를 보자

var fs = require('fs');

 

var contents = fs.readFile('hello.txt','utf-8',function(err,contents){

        console.log('read 1:'+contents);

});

이 코드는 hello.txt 라는 파일을 읽는 코드인데, 맨 뒤에 function(err.contents)라는 함수를 정의했다. 이 함수는 파일을 다 읽었을때 호출되는 callback 함수이다. fs.readFile을 호출하면, node는 파일이 다 읽을때 까지 이 코드에서 block되어 있는 것이 아니라 다음코드로 진행을 한다음, 파일을 다 읽으면 이벤트를 발생시켜서 여기에 연결된 function(err,contents)를 수행하게 되는 것이다.

 

node.js를 공부하다보면, 가장 큰 진입장벽중의 하나가, javascript node.js의 라이브러리를 새롭게 배우는 것보다, 기존의 procedural programming model에서 이러한 event driven programming의 개념을 익히는 것이 더 어렵다.

 

Event Emitter

그러면 이러한 이벤트를 어떻게 정의하고 처리할까? node.js에서 이벤트를 발생시키고 처리하는 방식은 EventEmitter 객체를 상속 받아서 구현한다.

 

라디오객체를 만들어서, on,off,change channel 이라는 이벤트가 발생했을때 각각 radoTurnOnCallBack,radioChangeChannelCallback, radioTurnOffCallback 함수가 각각 호출 되도록 해보자

 

먼저 Radio 객체를 만들어 보자

Radio = function(){

    events.EventEmitter.call(this); // call super class constructor

};

다음으로 Radio 객체를 EventEmitter로 부터 상속 받도록 하자.

클래스의 상속은 util 모듈의 inherits 메서드를 사용하면 된다.

util.inherits(Radio,events.EventEmitter);

 

다음으로, 호출될 callback 함수를 정의한 다음

// this is listener

var radioTurnOnListener = function(){

        util.debug('Radio turned on!!')

    }

var radioChangeChannelListener = function(channel){

        util.debug('Channel has been changed to '+ channel);

    }

var radioTurnOffListener = function(){

        util.debug('Radio turned off!!')

    }

radio 객체를 만들어서, 이 객체에 각각의 이벤트를 바인딩해보자

이벤트에 대한 바인딩은 emitter객체.on(‘이벤트명’,callback함수); 식으로 정의하면 된다.

radio.on('turnon',radioTurnOnListener);

radio.on('changechannel', radioChangeChannelListener);

radio.on('turnoff', radioTurnOffListener);

또는 event 바인딩시에, 함수명 대신 직접 함수를 다음과 같이도 정의할 수 있다.

radio.on('turnon', function(){

        util.debug('Radio turned on!!')

    });

 

이제 event가 바인딩 된 radio 객체가 생성되었다. 이제 이 객체에 이벤트를 날려보자

radio.emit('turnon');

radio.emit('changechannel');

radio.emit('turnoff');

다음은 실행 결과 이다.



전체 소스코드

var events = require('events');

var util = require('util');

 

// This is object that generate(emit) events

var Radio = function(){

    events.EventEmitter.call(this); // call super class constructor

 

};

util.inherits(Radio,events.EventEmitter);

 

// this is listener

var radioTurnOnListener = function(){

        util.debug('Radio turned on!!')

    }

var radioChangeChannelListener = function(channel){

        util.debug('Channel has been changed to '+ channel);

    }

var radioTurnOffListener = function(){

        util.debug('Radio turned off!!')

    }

 

var radio = new Radio();

 

radio.on('turnon',radioTurnOnListener);

radio.on('changechannel', radioChangeChannelListener);

radio.on('turnoff', radioTurnOffListener);

 

radio.emit('turnon');

radio.emit('changechannel');

radio.emit('turnoff');

 

Event Emitter methods

그러면 EventEmitter method들을 살펴보자..

Ÿ   emitter.addListener(eventname,listener function)

Ÿ   emitter.on(eventname,listener function)

이 메서드들은 eventname에 해당하는 이벤트에 대해서 ‘listener function’ 이름의 함수가 매번 호출 되도록 한다. 이벤트에 함수를 binding 할때는 하나의 이벤트에 여러개의 listener를 바인딩 할 수 있으며, 최대 바인딩 개수는 디폴트 값은 10개이다.

Ÿ  emitter.once(eventname,listener function)

이 메서드들은 eventname에 해당하는 이벤트에 대해서 ‘listener function’ 이름의 함수가 처음 한번만 호출 되도록 한다.

Ÿ  emitter.removeListener(eventname,listener function)

이 메서드는 “eventname”에 바인딩 되어 있는 “listener function” 이름의 함수와의 binding을 제거한다.

Ÿ  emitter.removeAllListener([eventnames])

인자는 배열형으로, 배열내에 들어가 있는 “eventnames”에 각각 바인딩 된 모든 함수에 대한 바인딩을 제거한다.

Ÿ  emitter.setMaxListeners(n)

해당 eventEmitter에 바인딩될 수 있는 이벤트의 수를 조정한다.

Ÿ  emitter.listeners(event)

“event”이름의 이벤트에 바인딩된 모든 callback 함수 이름을 리턴한다.

Ÿ  emitter.emit(eventname,[args])

“eventname”의 이벤트를 생성하고, 이벤트를 생성할 당시 [args]에 정의된 값 들을 이벤트와 함께 전달한다.

Module

모듈은 개념은, 다른 파일에서 모듈을 불러다 쓸 수 있는 일종의 라이브러리 개념이다.

java import되는 다른 클래스나 C에서 #include 되는 라이브러리의 개념을 생각하면 된다.

모듈은 파일 단위로 구현되는데, export를 이용하여, 외부에 노출된다. 마치 java class public method와 같은 개념으로 생각하면 된다. 해당 파일에 있는 함수라도 exports를 하지 않으면, 외부에서 호출할 수 없다. (일종의 java class private과 같은 개념)

Hello.js 파일에 hello라는 함수가 있고, 이를 다른 파일에서 불러쓰고 싶다면, Hello.js 파일에서 다음과 같이 정의한후

var hello = function(){...}

exports = hello;

hello 함수를 사용하고자 하는 파일 (예를 들어 app.js에서) require 를 이용해서 모듈을 불러오고, 호출해서 사용한다.

var hello = require('./Hello');

hello();

require에는 사용하고자 하는 모듈의 파일명을 “.js” 확장자를 제외 하고 서술한다.

모듈에서 exports 될 수 있는 것은 함수와 자바스크립트 객체가 된다. 위의 예는 함수 형태를 이용하여 모듈을 사용하는 경우인데, 만약에 객체형으로 export 하고 싶다면 export하는 파일에서는

exports.hello = function(){...}

export하고 불러 사용하는 쪽에서는

var h = require('./Hello');

h.hello();

형태로 호출한다.

Module의 경로

앞의 예에서는 “./Hello” Hello.js에서 .js를 제거하고 서술하였으며, 앞에 “./”를 이용하여 경로를 서술하였다. 파일의 경로를 아래와 같이 서술할 수 있는데,

var hello = require('Hello');

 

이 경우 node.js는 현재 실행 디렉토리를 먼저 찾고 없으면, 애플리케이션 디렉토리의 하위디렉토리인 /node_modules/ 라는 디렉토리를 찾는다.

이 디렉토리는 node의 모듈을 저장하는 디렉토리이다. 만약에 이 디렉토리에서 찾지 못하면, 하위 디렉토리의 /node_modules/ 디렉토리를 찾게 된다.

예를 들어서 애플리케이션 디렉토리가 /home/terry/myapp 인 경우,

/home/terry/myapp/node_modules를 먼저 찾고 없으면 다음과 같은 순서로 찾게 된다.

Ÿ  /home/terry/node_modules

Ÿ  /home/node_modules

Ÿ  /node_modules

 

 

Module의 종류

Native Module javascript 모듈

node.js는 엔진은 C++로 짜야져 있고, 그 위에서 동작하는 애플리케이션은 javascript로 구현된다. 그래서 모듈도 두 가지 타입을 가지고 있다. C++/C로 된 모듈을 Native 모듈이라고 하고, Javascript로 된 모듈을 javascript 모듈이라고 한다. Javascript 모듈의 경우, 설치시에 파일이 복사되는 수준에서 설치가 되지만, native module의 경우에는 컴파일을 하면서 설치를 한다. (마치 Linux make install 처럼). 그래서 반드시 C/C++ 컴파일러가 설치되어 있어야 한다. Linux에서는 GCC, Windows에서는 Visual Studio Expess(무료)등을 설치하면 된다.

Global Module Local Module

다음으로 Global Module Local Module이라는 개념을 가지고 있는데, Global Module은 시스템내에 설치된 모든 node.js 프로그램들이 참조할 수 있는 전역 모듈이다.

윈도우즈의 경우 디폴트로 ${user_home}/AppData/Roaming/npm/node_module 디렉토리에 설치된다.

또는 환경 변수 NODE_PATH에 그 경로를 다음과 같이 지정할 수 있다.

NODE_PATH=C:\Users\terry\AppData\Roaming\npm\node_module

Local Module의 경우 application 디렉토리의 /node_module 디렉토리에 설치되며, 해당 애플리케이션만 그 모듈을 참조할 수 있다.

기본 모듈과 확장 모듈

node의 모듈에도 node 설치시에 기본적으로 설치되는 모듈과, 추가로 설치해야 하는 확장 모듈이 있다. 기본 모듈은 http 프로토콜 핸들링이나, file system, event, cluster,TLS/SSL와 같은 암호화 등의 모듈이 있고, 확장 설치로는 웹 개발 프레임웍인 express등이 있다. 기본/확장 모듈에 대해서는 https://github.com/joyent/node/wiki/modules를 참고하기 바란다.


NPM

npm node package manager의 약자로, 앞서 설며한 모듈들에 대한 설치 및 의존성을 관리해 주는 도구이다. 마치 Linux rpm이나 Python pip 처럼 설치를 하면, repository에서 해당 모듈을 읽어다가 설치를 해주며, java maven처럼 package.json 이라는 파일에 (pom.xml과 비슷한 역할을 함) module간의 dependency (의존성)에 따라서 의존성이 있는 모듈을 같이 설치한다.

주요 명령어

여러가지 기능들이 있지만, 주요한 명령을 설명한다.

Ÿ      npm list {module} {-g} : 이 명령은 현재 디렉토리 아래에 설치되어 있는 확장 모듈을 리스트 해준다. {module}을 정해주면, 해당 모듈에 대한 리스트를 출력해주고, {-g} 옵션을 추가하면 global에 설치된 모듈 리스트들을 출력해준다.

Ÿ      npm install {-g} : npm 레파지토리 (maven 레파지토리 처럼 외부에 설치되어 있음)로부터, 모듈을 읽어서 로컬에 설치한다. –g 옵션을 적용할 경우 전역 모듈로 설치한다.

Ÿ      npm update {module} {-g} : 설치된 모듈을 최신 버전으로 업데이트 한다.

Ÿ      npm remove {module} {-g} : 설치된 모듈을 삭제한다.

Ÿ      npm info {module} : 해당 모듈의 의존성, 모듈명등 상세 정보를 출력한다.

Ÿ      npm init : 이 명령어를 수행하면 interactive prompt 모드를 통해서 package.json 파일을 만들기 위한 사용자로부터 받아서, package.json 파일을 생성해준다.

package.json 파일

package.json maven pom.xml과 같은 역할을 한다. 모듈에 대한 정보 (버전,제작자,모듈명 등) 기술을 하면서, 모듈에 대한 의존성, repository 경로등을 정의한다.

의존성 관리

아래 코드는 샘플 package.json 파일로, app이라는 모듈 0.0.1 버전에 대해서 서술하였으며, 이 모듈을 사용하기 위해서는 express 모듈 3.x버전과 redis 모듈 (버전에 상관없이 최신)을 필요로 한다. 또한 모듈은 git://xxx URL로부터 읽어서 설치하도록 되어 있다.

{

  "name": "app",

  "version": "0.0.1",

  "dependencies": {

    "express": "3.x",

    "redis": "*"

}

“repository”: {“type”:”git”,”url”,”git://xxxx”}

}

npm을 이용한 스크립트와 테스트 수행

npm make maven 처럼 custom command를 지정하여 명령어를 수행하도록 할 수 있다. 예를 들어 node server start하거나, test를 수행하거나 또는 빌드(?)패키징을 하도록 설정이 가능하다.

“scripts”라는 엘리먼트를 사용하면 되는데, 아래 예제는 npm start를 하면 app.js 애플리케이션으로 node.js를 실행하고, npm test를 하면, mocha 테스트 프레임웍을 수행하여,테스트를 수행하도록 하는 스크립트이다.

:중략

"redis": "*"

}

“scripts”: {“start”:”node app.js”,

             “test”:”mocha”}

}

참고

Npm 메뉴얼을 보면, “config’ 엘리먼트를 정의해놓고, 여기에 환경 변수를 설정할 수 있다.  db 접속 정보나, http listen 포트와 같이 환경에 따라서 변경이 되는 부분은 코드 상에 직접 넣지 않고, package.json 안에 설정해서, 이 파일만 변경을 하면 되도록 한다. 이를 통해서 개발,테스트 환경에 대해서package.json만 다르게 운영하거나, 운영 환경으로 배포시 간략하게 package.json만 변경하도록 한다.

예를 들어서 package.json

{ “config”:{“dbport”:”3306’,”dbuser”:”terry”}, ..} 라고 정해 놓으면

코드내에서 http.createServer(...).listen(process.env.npm_package_config_dbport); 라고 하면, package.json에서 지정한 환경 변수를 가져다 쓸 수 있다고 한다.

https://www.npmjs.org/doc/misc/npm-config.html 문서 참고

근데, 직접해보니 안된다.

 

그래서 사용하는 방법이 별도의 config 파일을 만들고 예를 들어 config.json으로 만들고

파일내에

{ “dbport”:”3306”,

“dbuser”:”terry”}

라고 해놓고

var config = require(“./config.json”); 으로 부르면 바로 json 객체로 나온다.

다음으로 값을 참고하려면

console.log(config.dbport);

console.log(config.dbuser);

식으로 사용하면 된다.

 

버전 Semantics

Module의 버전Semantics를 살펴보자. 보통 3자리로 구성되는데 1.2.3일 경우

Ÿ   1: major version

Ÿ   2: minor version

Ÿ   3:patch level

이다.package.json에서 dependency에 대한 버전을 정의할 수 있는데,

"dependencies" : {

   "mymodule" : "1.8.1

}

1.8.1 버전에 대한 의존성을

"dependencies" : {

   "mymodule" : "~1"

}

이 의미는 : >= 1.0.0 <2.0.0

"dependencies" : {

   "mymodule" : "~1.8”

}

이 의미는 : >=1.8 <2.0.0 까지

을 정하는 것인데, 설명은 했지만, node.js의 경우 한참 개발되고 있는 신생 에코 시스템이기 때문에 모듈 버전간의 변화가 심할 수 있기 때문에 되도록이면 range 방식은 사용하지 않는 것이 바람직하다.

회사 같은 곳에서 HTTP proxy를 사용하는 경우 해결 방법

npm install 인스톨시 회사 내부 네트워크에서 사용할 경우, 회사에서 proxy를 사용하면, npm install proxy 를 타지 않아서 설치가 npm 설치가 제대로 동작하지 않는 경우가 있다. 이런 문제를 해결 하려면,npm 환경 변수 세팅에 http proxy 서버를 지정해주면 되는데, 지정 방법은 다음과 같다.

Ÿ   npm config set proxy http://proxy.company.com:8080

Ÿ   npm config set https-proxy http://proxy.company.com:8080


다음 연재에서는 node.js의 웹 프레임웍인 Express에 대해서 소개하겠다.


#1 – node.js의 소개와 내부 구조 http://bcho.tistory.com/881

#2 - 설치와 개발환경 구축 http://bcho.tistory.com/884

#3 - Event,Module,NPM  http://bcho.tistory.com/885

#4 - 웹 개발 프레임웍 Express 1/2 - http://bcho.tistory.com/887




그리드형