Node.js EventEmitter 완벽 가이드
EventEmitter
는 Node.js의 핵심 기능 중 하나로, 이벤트 기반(Event-driven) 아키텍처를 구현할 수 있게 해주는 강력한 도구입니다. 이 가이드에서는 EventEmitter
의 개념과 사용법을 알아봅니다.
1. EventEmitter란? (핵심 개념)
EventEmitter
를 가장 쉽게 이해하는 방법은 **"방송국 시스템"**에 비유하는 것입니다.
- 이벤트 발생 (
emit
): 방송국이 특정 주제(이벤트 이름)로 프로그램을 송출하는 행위입니다. 이때 특정 데이터(값)를 함께 실어 보낼 수 있습니다. - 이벤트 구독 (
on
): 시청자가 특정 채널(이벤트 이름)을 구독하고 방송이 시작되기를 기다리는 행위입니다. - 이벤트 리스너 (Listener / 핸들러): 방송이 송출되면, 해당 채널을 구독하고 있던 시청자가 수행하는 **행동(콜백 함수)**입니다.
이 구조를 통해, 코드의 한 부분(발행자)이 다른 부분(구독자)을 직접 호출하지 않고도, "이벤트"라는 신호를 통해 느슨하게 상호작용할 수 있습니다. 이를 **느슨한 결합(Loose Coupling)**이라고 하며, 유연하고 확장 가능한 소프트웨어를 만드는 핵심 원칙입니다.
2. 기본 사용법
가장 기본적인 사용법은 EventEmitter
인스턴스를 직접 만들어 사용하는 것입니다.
const EventEmitter = require("events");
// 1. 방송국(EventEmitter) 인스턴스 생성
const myEmitter = new EventEmitter();
// 2. 'news' 채널 구독 신청
// 'news' 이벤트가 발생하면, 전달된 데이터를 받아 콘솔에 출력하는 리스너 등록
myEmitter.on("news", (data) => {
console.log("새로운 소식을 받았습니다:", data);
});
// 3. 'news' 채널로 방송 송출 (이벤트 발생)
console.log("방송을 시작합니다...");
myEmitter.emit("news", {
title: "Node.js 업데이트",
content: "새로운 버전이 출시되었습니다.",
});
console.log("방송을 종료합니다.");
// 실행 결과:
// 방송을 시작합니다...
// 새로운 소식을 받았습니다: { title: 'Node.js 업데이트', content: '새로운 버전이 출시되었습니다.' }
// 방송을 종료합니다.
3. 클래스와 함께 사용하기 (상속)
더 실용적인 방법은, 특정 역할을 하는 클래스가 EventEmitter
를 상속하여, 스스로 이벤트를 발생시키는 능력을 갖게 하는 것입니다.
예제 시나리오
파일을 처리하는 FileProcessor
라는 클래스가 있다고 가정해 봅시다. 이 클래스는 처리 시작, 진행 상황, 완료, 오류 등의 상태를 외부로 알리고 싶어 합니다.
3.1. 클래스 정의 (이벤트 발생자)
FileProcessor
클래스가 EventEmitter
를 상속받아, 처리 과정에서 start
, progress
, done
, error
이벤트를 emit
하도록 만듭니다.
FileProcessor.js
const EventEmitter = require("events");
class FileProcessor extends EventEmitter {
constructor(filePath) {
super(); // 부모 클래스(EventEmitter)의 생성자 호출은 필수입니다.
this.filePath = filePath;
}
process() {
// 1. 'start' 이벤트 발생
this.emit("start", this.filePath);
let progress = 0;
const interval = setInterval(() => {
progress += 25;
// 2. 'progress' 이벤트 발생 (진행률 데이터와 함께)
this.emit("progress", progress);
if (progress >= 100) {
clearInterval(interval);
// 3. 'done' 이벤트 발생
this.emit("done", { status: "success", processedFile: this.filePath });
}
}, 500);
// 가상의 오류 상황
if (!this.filePath) {
clearInterval(interval);
// 4. 'error' 이벤트 발생
this.emit("error", new Error("파일 경로가 제공되지 않았습니다."));
}
}
}
module.exports = FileProcessor;
3.2. 클래스 사용 (이벤트 구독자)
이제 다른 파일에서 FileProcessor
를 가져와 사용하면서, 각 이벤트에 대한 리스너를 등록합니다.
main.js
const FileProcessor = require("./FileProcessor.js");
const filePath = "my-document.txt";
const processor = new FileProcessor(filePath);
// [구독] 'start' 이벤트가 발생하면 실행될 리스너
processor.on("start", (path) => {
console.log(`파일 처리 시작: ${path}`);
});
// [구독] 'progress' 이벤트가 발생하면 실행될 리스너
processor.on("progress", (percent) => {
console.log(`진행률: ${percent}%`);
});
// [구독] 'done' 이벤트가 발생하면 실행될 리스너
processor.on("done", (result) => {
console.log(`처리 완료! 결과:`, result);
});
// [구독] 'error' 이벤트가 발생하면 실행될 리스너
processor.on("error", (err) => {
console.error(`오류 발생: ${err.message}`);
});
// 파일 처리 시작!
processor.process();
주요 메소드
emitter.on(eventName, listener)
: 지정된 이벤트(eventName
)를 구독합니다. 이벤트가 발생할 때마다listener
함수가 호출됩니다.emitter.emit(eventName, [...args])
: 지정된 이벤트(eventName
)를 발생시킵니다. 이벤트와 함께 추가적인 데이터(...args
)를 전달할 수 있습니다.emitter.once(eventName, listener)
: 이벤트를 단 한 번만 구독합니다. 리스너가 한번 실행된 후에는 자동으로 구독이 해제됩니다.emitter.off(eventName, listener)
또는emitter.removeListener(...)
: 등록했던 리스너를 제거합니다.
왜 사용하는가? (이점)
- 관심사의 분리 (Separation of Concerns):
FileProcessor
는 파일을 처리하는 자신의 핵심 로직에만 집중하고, "상황을 알리는" 역할만 합니다.main.js
는 처리 결과를 "어떻게 보여줄지(콘솔 출력 등)"에만 집중합니다.
- 유연성:
main.js
를 수정하지 않고도,FileProcessor
의 처리 방식(예: 진행률 간격 변경)을 자유롭게 바꿀 수 있습니다. 반대의 경우도 마찬가지입니다. - 재사용성:
FileProcessor
클래스는 콘솔 앱, 웹 서버, 데스크톱 앱 등 어떤 환경에서든 재사용할 수 있습니다. 사용하는 쪽에서 이벤트만 적절히 처리해주면 됩니다.