Node.js events (1)

잡담

Node.js의 특징인 Event driven과 Asynchronous를 잘 활용을 못하고 있는 것 같아서 Node.js의 매뉴얼 중 Events 항목을 한번 자세히 읽어 보기로 했다.
문서의 버전은 17-04-25 기준 최신인 v7.9.0 이다. 그럼 하나씩 훑어보자. 아니 자세히 보자.
영어 실력이 개판이니 내맘데로 번역해도 이해해주시기 바랍니다 :)

Events

매뉴얼을 들어가면 초록색 박스에 Stability:2 - Stable 이라고 되어 있다. 이 부분은 매뉴얼의 Stability Index 항목을 참고하면 된다.
간단하게 설명 하자면 Stability: NUMBER 에서 NUMBER 가 높을수록 안정된 API라는 의미이다. 사실 색상만 봐도 안정됬는지 안됬는지 알 수 있다. 0은 deprecated, 1 은 Experimental, 2는 stable 을 가리킨다.

다시, Events가 무엇인가?

많은 Node.js 코어 API들의 객체 내부에는 비동기적 이벤트 주도 아키텍쳐 로 작성이 되어 있는데, emitters 라고 불리는 이러한 객체들이 주기적으로 events 이름을 호출하고 이를 통해 Function 객체들이 호출되게 된다.

무슨 말인지 안다면 당신은 이미 Node.js를 많이 알고 있는 고수일 것이다.

예를 보자.

  • net.Server 객체에서는 peer가 접속을 할 때 마다 eventemit 한다.
  • fs.ReadStream 객체에서는 파일이 열리면 eventemit 한다.
  • stream 객체에서는 data가 읽을 준비가 되면 eventemit 한다.

즉, 해당 객체에서 처리해야 할 일이 준비가 되거나, 수행하게 되면 evnetemit, 간단하게 말하면 나 작업 (준비 혹은 완료) 다 됬어~ 라고 알려준다.

eventsemit 하는 모든 객체들은 EventEmitter 클래스의 인스턴스이다. 이러한 객체들은 EventEmitter.on() 함수를 통해 하나 이상의 함수를 해당 객체에 의해 생성된 events에다가 갖다 붙힐 수 있게 해준다. 아니, listeners로 등록할 수 있게 해준다.

event 이름은 camel-cased로 작성하는게 전형적인 방법 이지만 javascript의 property의 정상적인 key 값이면 사용할 수 있다.

그래서 event 이름을 Hi-Man 이라고 해봤는데 잘 작동 했다.

EventEmitter 객체가 eventemit하면 해당 이벤트에 연결된 모든 함수들이 동기적으로 수행된다. 그리고 호출된 listeners가 리턴한 값들은 무시된다.

무슨 이야기지? listeners? 일단 매뉴얼을 더 보도록 하자.

드디어 예제가 나왔다. 1개의 listeners를 가진 EventEmitter 인스턴스라는데 소스를 보면 좀 이해가 되지 않을까 싶다.

자 여기서! EventEmitter의 동작 방식을 설명해주고 있다.

1
2
The eventEmitter.on() method is used to register listeners,
while the eventEmitter.emit() method is used to trigger the event.

즉, eventEmitter.on() 메소드는 listeners를 등록하는데 사용되고,

eventEmitter.emit() 메소드는 해당 event를 작동시키는 trigger 로 사용된다.

소스코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');

최신 Node.js라서 그런지 ES6의 문법들로 작성이 되어 있다. 굉장히 보기 좋다.

소스를 보면, myEmitter.on()을 통해 console.log('an event occurred!') 를 출력하는 이벤트를 listeners로 등록했다.

그리고 맨 마지막에 eventEmitter.emit()을 통해 위의 listeners로 등록된 이벤트를 동작시키고 있다.

결과는 당연히 an event occurred! 가 콘솔에 찍힐 것이다.

listeners에 파라미터와 this 전달하기

eventEmitter.emit() 메소드는 파라미터를 임의로 전달 할 수 있도록 허락한다. 즉, 파라미터를 보내도 그만, 안보내도 그만이다.

하지만 여기서 기억해야 할 중요한 사항이 있는데, 평범한 listeners 함수가 eventEmitter에 의해 호출될 때, this 키워드는 의도적으로 listeners가 부착된 eventEmitter 클래스를 참조하게 된다.

말로만 봐서는 잘 모르겠으니 예제를 보자.

1
2
3
4
5
6
const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
console.log(a, b, this);
// Prints: a b {}
});
myEmitter.emit('event', 'a', 'b');

console.log(a, b, this)가 출력되면 this 는 무엇을 참조할까?

우리는 위에서 그 해답을 봤었다. 바로 myEmitter 를 참조한다. 꼭 기억해서 헷갈리지 않기를! 라고 말 했지만 사실 myEmitter는 객체이고, on()은 메소드이니 thismyEmitter를 참조하는건 당연한 것이다. 매뉴얼 오바하긴…

Asynchronous vs. Synchronous

EventEmitter 는 모든 listeners를 등록된 순서대로 동기적으로 호출한다. 이를 통해 이벤트의 적절한 순서와 race conditions 또는 로직 에러를 피할 수 있게 보장한다.

하지만 setImmediate() 또는 process.nextTick() 메소드를 통해서 listeners 함수를 Asynchronous 모드로 바꿀 수 있다.

1
2
3
4
5
6
7
const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
setImmediate(() => {
console.log('this happens asynchronously');
});
});
myEmitter.emit('event', 'a', 'b');

위 소스만 봐서는 정말로 Asynchronous 모드로 동작 하는지 잘 모르겠다. 소스코드를 좀 수정해서 돌려보자.

1
2
3
4
5
6
7
8
9
const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
setImmediate(() => {
console.log('this happens asynchronously');
});
});
console.log('before emit');
myEmitter.emit('event', 'a', 'b');
console.log('after emit');

실행 결과는 다음과 같다.

1
2
3
before emit
after emit
this happens asynchronously

그럼 setImmediate() 없이 수행하면 어떻게 될까? 이렇게 나온다.

1
2
3
before emit
this happens asynchronously
after emit

와우! lol

events 를 딱 한번만 처리하기

위에서 listeners 등록할 때 on() 메소드를 사용했는데 이거 말고 once() 라는 메소드를 쓰면 아무리 emit()을 해도 딱 한 번만 호출되고 해당 listeners가 제거되어 버린다. 예제는 아래와 같다.

1
2
3
4
5
6
7
8
9
const myEmitter = new MyEmitter();
let m = 0;
myEmitter.once('event', () => {
console.log(++m);
});
myEmitter.emit('event');
// Prints: 1
myEmitter.emit('event');
// Ignored

Error events

EventEmitter 인스턴스에서 error가 발생하면, 보통은 error event를 emit 하게 만든다.

만약에 EventEmittererror 관련 listeners가 하나도 등록이 되어 있지 않는데 error가 emit 되면 어떻게 될까?

error 가 던져지고, stack trace가 출력되고, Node process가 죽어버린다.

1
2
3
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
// Throws and crashes Node.js

이를 방지하려면 3 가지 방법이 있다.

  • process 객체에 uncaughtException으로 listeners를 등록한다.
  • domain 모듈을 사용한다 - deprecated
  • 해당 객체에 항상 errorlisteners를 등록한다. - BEST!
1
2
3
4
5
6
7
8
const myEmitter = new MyEmitter();

process.on('uncaughtException', (err) => {
console.error('whoops! there was an error');
});

myEmitter.emit('error', new Error('whoops!'));
// Prints: whoops! there was an error

이거 보다는

1
2
3
4
5
6
const myEmitter = new MyEmitter();
myEmitter.on('error', (err) => {
console.error('whoops! there was an error');
});
myEmitter.emit('error', new Error('whoops!'));
// Prints: whoops! there was an error

이렇게 쓰자.