Node.js 인터뷰 질문 47
질문: Node.js의 EventEmitter 클래스에 대해 설명하고, 이를 활용한 이벤트 기반 프로그래밍의 예시를 들어주세요.
답변:
Node.js의 EventEmitter는 이벤트 기반 프로그래밍의 핵심 클래스로, Node.js의 많은 내장 모듈이 이 클래스를 상속받아 이벤트 기반 아키텍처를 구현합니다. EventEmitter를 사용하면 이벤트를 발생(emit)시키고 이벤트 리스너(listener)를 등록하여 비동기적으로 작업을 처리할 수 있습니다.
EventEmitter의 기본 개념
EventEmitter는 이벤트를 발생시키고 이벤트에 반응하는 콜백 함수(리스너)를 등록하는 메커니즘을 제공합니다. 이는 발행-구독(Publish-Subscribe) 패턴의 구현체라고 볼 수 있습니다.
주요 메서드:
emitter.on(eventName, listener)
: 이벤트 리스너 등록emitter.emit(eventName[, ...args])
: 이벤트 발생emitter.once(eventName, listener)
: 한 번만 실행되는 이벤트 리스너 등록emitter.removeListener(eventName, listener)
: 특정 이벤트 리스너 제거emitter.removeAllListeners([eventName])
: 모든 리스너 제거emitter.listeners(eventName)
: 특정 이벤트의 리스너 배열 반환emitter.listenerCount(eventName)
: 특정 이벤트의 리스너 개수 반환
기본 사용법
const EventEmitter = require("events");
// EventEmitter 인스턴스 생성
const myEmitter = new EventEmitter();
// 이벤트 리스너 등록
myEmitter.on("event", function (a, b) {
console.log("이벤트 발생!", a, b);
});
// 이벤트 발생
myEmitter.emit("event", "a", "b");
// 출력: 이벤트 발생! a b
한 번만 실행되는 이벤트 리스너
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 한 번만 실행되는 이벤트 리스너 등록
myEmitter.once("oneTimeEvent", () => {
console.log("이 리스너는 한 번만 호출됩니다.");
});
// 이벤트 두 번 발생
myEmitter.emit("oneTimeEvent"); // 출력: 이 리스너는 한 번만 호출됩니다.
myEmitter.emit("oneTimeEvent"); // 아무것도 출력되지 않음
이벤트 리스너 제거
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
function listener1() {
console.log("리스너 1 실행");
}
function listener2() {
console.log("리스너 2 실행");
}
// 이벤트 리스너 등록
myEmitter.on("event", listener1);
myEmitter.on("event", listener2);
// 이벤트 발생
myEmitter.emit("event");
// 출력:
// 리스너 1 실행
// 리스너 2 실행
// 특정 리스너 제거
myEmitter.removeListener("event", listener1);
// 또는 myEmitter.off('event', listener1); (Node.js 10.0.0 이상)
// 이벤트 다시 발생
myEmitter.emit("event");
// 출력: 리스너 2 실행
에러 이벤트 처리
EventEmitter에서 'error' 이벤트는 특별하게 처리됩니다. 'error' 이벤트에 대한 리스너가 없으면, Node.js는 오류를 던지고 프로세스를 종료할 수 있습니다.
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 'error' 이벤트 리스너 등록
myEmitter.on("error", (err) => {
console.error("에러 발생:", err.message);
});
// 에러 이벤트 발생
myEmitter.emit("error", new Error("에러가 발생했습니다!"));
// 출력: 에러 발생: 에러가 발생했습니다!
클래스 상속을 통한 EventEmitter 사용
일반적으로 EventEmitter는 클래스 상속을 통해 사용됩니다. 이를 통해 자신만의 이벤트를 발생시키는 클래스를 만들 수 있습니다.
const EventEmitter = require("events");
// EventEmitter를 상속받는 클래스 생성
class MyStream extends EventEmitter {
constructor() {
super();
}
write(data) {
this.emit("data", data);
}
}
// 인스턴스 생성
const stream = new MyStream();
// 'data' 이벤트 리스너 등록
stream.on("data", (data) => {
console.log(`데이터 수신: ${data}`);
});
// 데이터 쓰기 및 이벤트 발생
stream.write("Hello, world!");
// 출력: 데이터 수신: Hello, world!
비동기 이벤트 처리
이벤트는 비동기 작업을 처리하는 데 매우 유용합니다. 다음은 파일 읽기 작업이 완료되면 이벤트를 발생시키는 예제입니다.
const EventEmitter = require("events");
const fs = require("fs");
class FileReader extends EventEmitter {
readFile(file) {
fs.readFile(file, "utf8", (err, data) => {
if (err) {
this.emit("error", err);
return;
}
this.emit("fileRead", data);
});
}
}
const reader = new FileReader();
reader.on("fileRead", (data) => {
console.log("파일 내용:", data);
});
reader.on("error", (err) => {
console.error("파일 읽기 오류:", err.message);
});
// 파일 읽기 시작
reader.readFile("example.txt");
이벤트 리스너 실행 순서
이벤트 리스너는 등록된 순서대로 실행됩니다. 특정 리스너를 다른 리스너보다 먼저 실행하려면 prependListener
메서드를 사용할 수 있습니다.
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 첫 번째 리스너 등록
myEmitter.on("event", () => {
console.log("리스너 1 실행");
});
// 두 번째 리스너 등록
myEmitter.on("event", () => {
console.log("리스너 2 실행");
});
// 세 번째 리스너를 리스너 목록의 앞부분에 추가
myEmitter.prependListener("event", () => {
console.log("리스너 3 실행 (앞부분에 추가)");
});
// 이벤트 발생
myEmitter.emit("event");
// 출력:
// 리스너 3 실행 (앞부분에 추가)
// 리스너 1 실행
// 리스너 2 실행
Node.js 내장 모듈의 EventEmitter 사용 예시
Node.js의 많은 내장 모듈이 EventEmitter를 상속합니다. 대표적인 예로 http.Server
, fs.ReadStream
, net.Socket
등이 있습니다.
1. HTTP 서버 예제
const http = require("http");
// http.Server는 EventEmitter를 상속받습니다
const server = http.createServer();
// 'request' 이벤트 리스너 등록
server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World\n");
});
// 'listening' 이벤트 리스너 등록
server.on("listening", () => {
console.log("서버가 포트 3000에서 실행 중입니다.");
});
// 서버 시작
server.listen(3000);
2. 파일 스트림 예제
const fs = require("fs");
// fs.ReadStream은 EventEmitter를 상속받습니다
const readStream = fs.createReadStream("example.txt");
readStream.on("open", (fd) => {
console.log(`파일이 열렸습니다. 파일 디스크립터: ${fd}`);
});
readStream.on("data", (chunk) => {
console.log(`${chunk.length} 바이트 데이터 수신`);
});
readStream.on("end", () => {
console.log("파일 읽기 완료");
});
readStream.on("error", (err) => {
console.error("파일 읽기 오류:", err);
});
실제 사용 사례
1. 작업 진행 상황 보고
작업이 진행됨에 따라 진행 상황을 보고하는 이벤트를 발생시키는 예제입니다.
const EventEmitter = require("events");
class TaskRunner extends EventEmitter {
constructor(tasks) {
super();
this.tasks = tasks;
}
run() {
this.emit("start", this.tasks.length);
let completed = 0;
// 각 작업을 비동기적으로 실행
this.tasks.forEach((task, index) => {
// 작업 시뮬레이션 (실제로는 데이터베이스 쿼리, API 호출 등)
setTimeout(() => {
const result = task();
completed++;
// 작업 완료 이벤트 발생
this.emit("task_complete", {
taskIndex: index,
taskResult: result,
completed: completed,
total: this.tasks.length,
});
// 모든 작업이 완료되면 'end' 이벤트 발생
if (completed === this.tasks.length) {
this.emit("end");
}
}, Math.random() * 1000); // 랜덤 지연 시간
});
}
}
// 사용 예시
const tasks = [
() => "작업 1 결과",
() => "작업 2 결과",
() => "작업 3 결과",
() => "작업 4 결과",
() => "작업 5 결과",
];
const runner = new TaskRunner(tasks);
runner.on("start", (totalTasks) => {
console.log(`총 ${totalTasks}개 작업 시작`);
});
runner.on("task_complete", (info) => {
const percent = Math.round((info.completed / info.total) * 100);
console.log(`작업 ${info.taskIndex + 1} 완료: ${info.taskResult}`);
console.log(`진행률: ${percent}% (${info.completed}/${info.total})`);
});
runner.on("end", () => {
console.log("모든 작업 완료!");
});
// 작업 실행
runner.run();
2. 사용자 정의 스트림 구현
EventEmitter를 사용하여 사용자 정의 스트림을 구현하는 예제입니다.
const EventEmitter = require("events");
// 간단한 데이터 변환 스트림 구현
class StringTransformer extends EventEmitter {
constructor() {
super();
this.buffer = "";
}
// 데이터 입력 처리
write(data) {
// 데이터 버퍼링
this.buffer += data;
// 라인 단위로 처리
const lines = this.buffer.split("\n");
this.buffer = lines.pop(); // 마지막 불완전한 라인 저장
lines.forEach((line) => {
// 데이터 변환 및 'data' 이벤트 발생
const transformed = line.toUpperCase();
this.emit("data", transformed);
});
return true;
}
// 스트림 종료
end(data) {
if (data) {
this.write(data);
}
// 마지막 버퍼 처리
if (this.buffer) {
const transformed = this.buffer.toUpperCase();
this.emit("data", transformed);
this.buffer = "";
}
this.emit("end");
}
}
// 사용 예시
const transformer = new StringTransformer();
transformer.on("data", (chunk) => {
console.log("변환된 데이터:", chunk);
});
transformer.on("end", () => {
console.log("변환 완료");
});
// 데이터 처리
transformer.write("hello\nworld\nnode");
transformer.end(".js is awesome!");
// 출력:
// 변환된 데이터: HELLO
// 변환된 데이터: WORLD
// 변환된 데이터: NODE.JS IS AWESOME!
// 변환 완료
3. 실시간 알림 시스템
이벤트 기반 아키텍처를 사용하여 실시간 알림 시스템을 구현하는 예제입니다.
const EventEmitter = require("events");
// 알림 관리자 클래스
class NotificationManager extends EventEmitter {
constructor() {
super();
this.users = new Map(); // 사용자별 구독 정보 저장
}
// 사용자 구독 등록
subscribe(userId, channels) {
if (!this.users.has(userId)) {
this.users.set(userId, new Set());
}
channels.forEach((channel) => {
this.users.get(userId).add(channel);
});
this.emit("user_subscribed", { userId, channels });
}
// 사용자 구독 취소
unsubscribe(userId, channels) {
if (!this.users.has(userId)) return;
channels.forEach((channel) => {
this.users.get(userId).delete(channel);
});
this.emit("user_unsubscribed", { userId, channels });
}
// 특정 채널에 알림 발송
notify(channel, message) {
// 해당 채널을 구독한 모든 사용자 찾기
const notifiedUsers = [];
this.users.forEach((subscribedChannels, userId) => {
if (subscribedChannels.has(channel)) {
// 실제로는 여기서 사용자에게 알림 전송
// (푸시 알림, 이메일, 웹소켓 메시지 등)
notifiedUsers.push(userId);
}
});
this.emit("notification_sent", {
channel,
message,
userCount: notifiedUsers.length,
users: notifiedUsers,
});
return notifiedUsers;
}
}
// 사용 예시
const notifier = new NotificationManager();
// 이벤트 리스너 등록
notifier.on("user_subscribed", (data) => {
console.log(
`사용자 ${data.userId}가 ${data.channels.join(", ")} 채널을 구독했습니다.`
);
});
notifier.on("notification_sent", (data) => {
console.log(`채널 ${data.channel}에 알림 발송: "${data.message}"`);
console.log(
`총 ${data.userCount}명의 사용자에게 전송됨: ${data.users.join(", ")}`
);
});
// 사용자 구독 등록
notifier.subscribe("user1", ["news", "sports"]);
notifier.subscribe("user2", ["news", "tech"]);
notifier.subscribe("user3", ["sports", "entertainment"]);
// 알림 발송
notifier.notify("news", "주요 뉴스: Node.js 17 출시!");
notifier.notify("sports", "스포츠 소식: 올림픽 개막!");
EventEmitter 사용 시 주의사항
메모리 누수 방지: 이벤트 리스너를 너무 많이 등록하면 메모리 누수가 발생할 수 있습니다.
removeListener
를 사용하여 더 이상 필요하지 않은 리스너를 제거하세요.리스너 제한: 기본적으로 하나의 이벤트에 최대 10개의 리스너를 등록할 수 있습니다. 더 많은 리스너가 필요한 경우
emitter.setMaxListeners(n)
을 사용하여 제한을 높일 수 있습니다.
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 최대 리스너 수 확인
console.log(myEmitter.getMaxListeners()); // 기본값: 10
// 최대 리스너 수 변경
myEmitter.setMaxListeners(20);
console.log(myEmitter.getMaxListeners()); // 20
- 비동기 실행: 이벤트 리스너는 동기적으로 호출됩니다. 긴 작업이나 I/O 작업은 프로세스를 차단할 수 있으므로,
setImmediate()
나process.nextTick()
을 사용하여 비동기적으로 실행하는 것이 좋습니다.
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 비동기 이벤트 리스너
myEmitter.on("event", (data) => {
setImmediate(() => {
// 시간이 오래 걸리는 작업
console.log("비동기적으로 처리:", data);
});
});
myEmitter.emit("event", "some data");
console.log("이벤트 발생 후 즉시 실행");
// 출력 순서:
// 1. 이벤트 발생 후 즉시 실행
// 2. 비동기적으로 처리: some data
- 에러 이벤트 처리: 항상 'error' 이벤트 리스너를 등록하여 예상치 못한 프로세스 종료를 방지하세요.
요약
Node.js의 EventEmitter는 이벤트 기반 프로그래밍을 위한 핵심 클래스로, 다음과 같은 특징을 가집니다:
이벤트 발행-구독 패턴: 이벤트를 발생시키고(emit) 이벤트 리스너를 등록하여 처리합니다.
비동기 작업 처리: 이벤트를 통해 비동기 작업을 효과적으로 관리할 수 있습니다.
내장 모듈과의 통합: Node.js의 많은 내장 모듈이 EventEmitter를 상속받아 이벤트 기반 인터페이스를 제공합니다.
유연한 이벤트 관리: 다양한 메서드를 통해 이벤트 리스너를 추가, 제거, 관리할 수 있습니다.
EventEmitter는 복잡한 비동기 워크플로우를 관리하거나, 상태 변화를 알리거나, 느슨하게 결합된 컴포넌트 간의 통신을 구현하는 데 매우 유용합니다. Node.js의 비동기 이벤트 기반 아키텍처를 효과적으로 활용하기 위해서는 EventEmitter의 사용법과 패턴을 잘 이해하는 것이 중요합니다.