EventEmitter는 Node.js events 모듈에 있는 클래스로, 객체가 이름 붙은 이벤트를 알릴 수 있게 해 줍니다. emitter.on('name', fn)으로 리스너를 등록하고 emitter.emit('name', ...args)로 발생시킵니다. 그 이름의 리스너가 모두, 추가된 순서대로, 넘긴 인자와 함께 실행됩니다. HTTP 서버, 스트림, process가 모두 EventEmitter라서 같은 몇 가지 메서드가 Node.js 어디서나 통합니다.
두 리스너 모두 before emit과 after emit 사이에서 실행됩니다. emit은 리스너를 동기적으로 호출하고, 그것이 끝나야 반환합니다. 두 shop.on 호출을 바꾸면 가운데 두 줄도 바뀝니다.
on, once, off
on은 제거할 때까지 리스너를 유지하고, once는 첫 호출 뒤에 제거하며, off(removeListener와 같음)는 하나를 제거합니다. 리스너를 제거하려면 추가했던 바로 그 함수가 필요하므로, 화살표 함수를 즉석에서 넘기지 말고 이름을 붙이세요.
first open ever는 Ada 다음에 한 번만 출력됩니다. logOpen을 제거하자 아무도 듣지 않으므로 마지막 emit은 false를 반환합니다. door.removeAllListeners('open')을 썼다면 두 리스너를 한 번에 지웠을 것입니다.
| 메서드 | 하는 일 |
|---|---|
on(name, fn), addListener | emit할 때마다 fn 호출 |
once(name, fn) | 다음 emit에서만 fn 호출 |
prependListener(name, fn) | on과 같지만 기존 리스너보다 먼저 실행 |
off(name, fn), removeListener | 그 함수를 제거 |
emit(name, ...args) | 리스너를 호출하고, 하나라도 있으면 true 반환 |
listenerCount(name) | 그 이벤트의 리스너 개수 |
error 이벤트
'error'는 특별합니다. 아무도 듣지 않는데 내보내면 emit이 그 에러를 던지고, 실제 프로그램에서 잡히지 않은 에러는 프로세스를 죽입니다. 스트림이나 서버처럼 실패할 수 있는 emitter에는 항상 'error' 리스너를 추가하세요.
리스너가 없으면 emit이 에러를 던지고 catch가 받습니다. 리스너를 추가하면 같은 emit이 그 리스너를 호출할 뿐이고 프로그램은 계속됩니다. 다른 이벤트 이름은 리스너가 없으면 조용히 무시됩니다.
내 클래스에서 EventEmitter 상속하기
가장 흔한 사용법은 내 클래스가 EventEmitter를 상속하고, 생성자에서 super()를 호출한 뒤, 무슨 일이 생기면 this.emit을 호출하는 것입니다. 그러면 호출하는 쪽은 다른 Node.js 객체처럼 내 객체를 구독할 수 있습니다.
25%, 50%, 75%, 100%, 그리고 photo.png uploaded가 출력됩니다. up.start()를 두 up.on 줄 위로 옮기면 아무것도 출력되지 않습니다. 누가 듣기 전에 이벤트가 발생하기 때문입니다.
생성자 안에서 이벤트를 내보낼 때도 같은 함정이 있습니다. 즉시 내보내야 하는 라이브러리는 그 호출을 process.nextTick으로 감싸는데, 이것은 호출하는 쪽의 on 줄보다 뒤에 실행됩니다(이벤트 루프 페이지 참고).
events.once로 이벤트 기다리기
events.once(emitter, name)은 다음에 이벤트가 발생하면 그 인자를 배열로 담아 resolve되는 Promise를 반환합니다. 한 번뿐인 이벤트를 await할 수 있는 것으로 바꿔 주고, emitter가 먼저 'error'를 내보내면 reject됩니다.
await는 타이머가 ready를 내보낼 때까지 멈췄다가, 배열을 두 인자로 구조 분해합니다. 여러 번 발생하는 이벤트라면 events.on(emitter, name)이 for await로 돌 수 있는 비동기 이터레이터를 줍니다.
흔한 실수
자주 실행되는 함수 안에서 리스너를 추가하는 것. 요청 핸들러마다 on을 호출하면 리스너와 메모리가 쌓입니다. 한 번만 추가하거나 once를 쓰세요.
즉석 함수를 제거하려는 것. off('data', () => ...)는 아무것도 제거하지 않습니다. 그 화살표 함수는 새로 만든 함수이기 때문입니다. 추가한 함수의 참조를 보관하세요.
리스너가 나중에 실행될 거라고 기대하는 것. emit은 동기적이라서 느린 리스너는 이벤트를 내보낸 코드를 붙잡습니다. 리스너를 async로 표시해도 소용없습니다. 첫 await까지는 그 코드가 여전히 같은 스레드에서 실행되기 때문입니다. 무거운 작업은 사이사이에 setImmediate를 넣어 여러 조각으로 나누거나 워커 스레드로 옮기세요.
자주 묻는 질문
EventEmitter는 비동기인가요?
아닙니다. emit은 모든 리스너를 그 자리에서 하나씩 차례로 호출하고, 마지막 리스너가 끝나면 반환합니다. 리스너 안의 코드가 비동기 작업을 시작할 수는 있지만, 리스너 자체는 동기적으로 호출됩니다.
MaxListenersExceededWarning은 무슨 뜻인가요?
한 이벤트에 리스너가 10개를 넘게 추가되면 Node.js가 경고합니다. 요청마다 리스너를 추가하고 제거하지 않는 경우가 많기 때문입니다. 남는 리스너를 제거하거나, 정말 더 필요하다면 emitter.setMaxListeners(n)을 호출하세요.
emit은 무엇을 반환하나요?
이벤트에 리스너가 하나 이상 있으면 true, 아무도 듣지 않으면 false입니다. 리스너가 반환한 값은 반환하지 않습니다.
Node.js의 어떤 객체가 EventEmitter인가요?
Node.js 핵심 객체 대부분입니다. HTTP 서버, 요청과 응답, 스트림, 소켓, process, readline 인터페이스. 그래서 모두 on, once, off를 똑같이 가지고 있습니다.