Node.js 인터뷰 질문 40
질문: Node.js의 EventEmitter 클래스에 대해 설명하고, 어떻게 사용하는지 예제 코드와 함께 설명해주세요.
답변:
EventEmitter는 Node.js에서 이벤트 기반 프로그래밍의 핵심 요소로, 이벤트를 발생(emit)하고 처리(listen)할 수 있는 기능을 제공합니다. 이는 Node.js의 비동기 이벤트 기반 아키텍처의 근간을 이루며, 많은 Node.js 핵심 모듈들이 EventEmitter를 상속하여 사용합니다.
EventEmitter 기본 개념
EventEmitter는 다음과 같은 기본 원칙으로 작동합니다:
- 이벤트 발신(emit): 특정 이벤트가 발생했음을 알림
- 이벤트 수신(on/listen): 특정 이벤트에 대한 리스너(콜백 함수) 등록
- 이벤트 처리: 이벤트가 발생했을 때 등록된 리스너 실행
기본 사용법
EventEmitter를 사용하는 가장 기본적인 방법은 다음과 같습니다:
const EventEmitter = require("events");
// EventEmitter 인스턴스 생성
const myEmitter = new EventEmitter();
// 'event' 이벤트에 대한 리스너 등록
myEmitter.on("event", () => {
console.log("이벤트가 발생했습니다!");
});
// 'event' 이벤트 발생
myEmitter.emit("event");
// 출력: 이벤트가 발생했습니다!
매개변수를 가진 이벤트
이벤트 발생 시 리스너에 데이터를 전달할 수 있습니다:
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 매개변수를 받는 리스너 등록
myEmitter.on("userJoined", (id, username) => {
console.log(`사용자 접속: ${username}님(ID: ${id})`);
});
// 매개변수와 함께, 'userJoined' 이벤트 발생
myEmitter.emit("userJoined", 1, "홍길동");
// 출력: 사용자 접속: 홍길동님(ID: 1)
주요 메소드
EventEmitter 클래스는 다양한 메소드를 제공합니다:
1. 이벤트 리스너 관련 메소드
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 리스너 추가
function listener1() {
console.log("리스너 1 실행");
}
function listener2() {
console.log("리스너 2 실행");
}
// 'connection' 이벤트에 리스너 등록
myEmitter.on("connection", listener1);
myEmitter.on("connection", listener2);
// 등록된 리스너 개수 확인
console.log(
`'connection' 이벤트 리스너 개수: ${myEmitter.listenerCount("connection")}`
);
// 이벤트 발생
myEmitter.emit("connection");
// 출력:
// 리스너 1 실행
// 리스너 2 실행
// 리스너 제거
myEmitter.removeListener("connection", listener1);
console.log(
`'connection' 이벤트 리스너 개수: ${myEmitter.listenerCount("connection")}`
);
// 모든 리스너 제거
myEmitter.removeAllListeners("connection");
console.log(
`'connection' 이벤트 리스너 개수: ${myEmitter.listenerCount("connection")}`
);
2. 일회성 이벤트 리스너
once()
메소드를 사용하면 이벤트가 한 번만 처리되는 리스너를 등록할 수 있습니다:
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 일회성 이벤트 리스너 등록
myEmitter.once("onetime", () => {
console.log("이 리스너는 한 번만 호출됩니다");
});
// 이벤트 두 번 발생
myEmitter.emit("onetime");
// 출력: 이 리스너는 한 번만 호출됩니다
myEmitter.emit("onetime");
// 출력 없음 (리스너가 이미 제거됨)
3. 오류 처리
'error' 이벤트는 EventEmitter에서 특별하게 처리됩니다. 'error' 이벤트가 발생했는데 리스너가 없으면 프로그램이 종료됩니다:
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// error 이벤트 리스너 등록
myEmitter.on("error", (err) => {
console.error("오류 발생:", err.message);
});
// 'error' 이벤트 발생
myEmitter.emit("error", new Error("문제가 발생했습니다"));
// 출력: 오류 발생: 문제가 발생했습니다
EventEmitter 상속하기
사용자 정의 클래스에서 EventEmitter를 상속하여 사용할 수 있습니다:
const EventEmitter = require("events");
// EventEmitter를 상속한 사용자 정의 클래스
class ChatRoom extends EventEmitter {
constructor() {
super();
this.users = [];
}
// 사용자 입장 메소드
join(username) {
this.users.push(username);
// 'userJoined' 이벤트 발생
this.emit("userJoined", username);
}
// 메시지 전송 메소드
sendMessage(username, message) {
// 'newMessage' 이벤트 발생
this.emit("newMessage", username, message);
}
}
// 인스턴스 생성 및 사용
const chatRoom = new ChatRoom();
// 이벤트 리스너 등록
chatRoom.on("userJoined", (username) => {
console.log(`${username}님이 채팅방에 입장했습니다`);
});
chatRoom.on("newMessage", (username, message) => {
console.log(`${username}: ${message}`);
});
// 메소드 호출로 이벤트 발생
chatRoom.join("홍길동");
// 출력: 홍길동님이 채팅방에 입장했습니다
chatRoom.sendMessage("홍길동", "안녕하세요!");
// 출력: 홍길동: 안녕하세요!
비동기 처리와 EventEmitter
EventEmitter는 비동기 작업의 진행 상황을 알리는 데 유용합니다:
const EventEmitter = require("events");
const fs = require("fs");
class FileProcessor extends EventEmitter {
processFile(filePath) {
this.emit("start", filePath);
// 파일 읽기 (비동기)
fs.readFile(filePath, "utf8", (err, content) => {
if (err) {
this.emit("error", err);
return;
}
this.emit("data", content);
// 파일 처리 (예: 데이터 변환)
const processedData = content.toUpperCase();
this.emit("processed", processedData);
// 처리 완료
this.emit("end");
});
return this; // 메소드 체이닝을 위해 인스턴스 반환
}
}
// 사용 예
const processor = new FileProcessor();
processor
.on("start", (filePath) => {
console.log(`파일 처리 시작: ${filePath}`);
})
.on("data", (data) => {
console.log(`파일 내용 크기: ${data.length} 바이트`);
})
.on("processed", (data) => {
console.log(`처리된 데이터 일부: ${data.substring(0, 20)}...`);
})
.on("error", (err) => {
console.error(`오류 발생: ${err.message}`);
})
.on("end", () => {
console.log("파일 처리 완료");
});
// 파일 처리 시작
processor.processFile("example.txt");
유용한 패턴들
1. 이벤트 이름 상수화
이벤트 이름을 상수로 정의하여 오타를 방지하고 코드의 가독성을 높일 수 있습니다:
const EventEmitter = require("events");
// 이벤트 이름 상수
const EVENTS = {
CONNECTION: "connection",
DATA_RECEIVED: "dataReceived",
END: "end",
ERROR: "error",
};
class Server extends EventEmitter {
constructor() {
super();
}
start() {
// 연결 시작 이벤트 발생
this.emit(EVENTS.CONNECTION);
// 데이터 수신 시뮬레이션
setTimeout(() => {
this.emit(EVENTS.DATA_RECEIVED, { data: "some data" });
}, 1000);
// 종료 시뮬레이션
setTimeout(() => {
this.emit(EVENTS.END);
}, 2000);
}
}
const server = new Server();
server.on(EVENTS.CONNECTION, () => {
console.log("연결되었습니다");
});
server.on(EVENTS.DATA_RECEIVED, (data) => {
console.log("데이터 수신:", data);
});
server.on(EVENTS.END, () => {
console.log("연결이 종료되었습니다");
});
server.start();
2. 이벤트 버블링/위임
컴포넌트 계층 구조에서 하위 컴포넌트의 이벤트를 상위 컴포넌트로 전달하는 패턴:
const EventEmitter = require("events");
// 부모 컴포넌트
class App extends EventEmitter {
constructor() {
super();
this.database = new Database(this);
}
}
// 자식 컴포넌트
class Database extends EventEmitter {
constructor(parent) {
super();
this.parent = parent;
// 자체 이벤트를 부모에게 전달
this.on("query", (query) => {
this.parent.emit("database:query", query);
});
}
executeQuery(sql) {
console.log(`SQL 실행: ${sql}`);
this.emit("query", { sql, timestamp: new Date() });
return { results: [] };
}
}
// 사용 예
const app = new App();
// 애플리케이션 레벨에서 데이터베이스 쿼리 이벤트 감지
app.on("database:query", (queryInfo) => {
console.log(`쿼리 로깅: ${queryInfo.sql}`);
});
// 데이터베이스 쿼리 실행
app.database.executeQuery("SELECT * FROM users");
주의사항과 모범 사례
1. 메모리 누수 방지
이벤트 리스너가 너무 많이 등록되면 메모리 누수가 발생할 수 있습니다. Node.js는 기본적으로 한 이벤트에 10개 이상의 리스너가 등록되면 경고를 출력합니다:
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
// 최대 리스너 수 변경 (필요한 경우에만)
myEmitter.setMaxListeners(20);
// 현재 최대 리스너 수 확인
console.log(myEmitter.getMaxListeners()); // 20
2. 이벤트 리스너 관리
이벤트 리스너를 관리하지 않으면 메모리 누수가 발생할 수 있습니다. 필요하지 않은 리스너는 제거하는 것이 좋습니다:
const EventEmitter = require("events");
const myEmitter = new EventEmitter();
function onEvent() {
console.log("이벤트 발생");
}
// 리스너 등록
myEmitter.on("event", onEvent);
// 작업 수행 후 필요 없어진 리스너 제거
myEmitter.removeListener("event", onEvent);
// 또는
// myEmitter.off('event', onEvent); // Node.js 10 이상에서 사용 가능
3. 이벤트 이름 충돌 방지
이벤트 이름의 충돌을 방지하기 위해 네임스페이스 패턴을 사용할 수 있습니다:
// 네임스페이스 패턴 (콜론으로 구분)
myEmitter.on("database:connection", () => {
console.log("데이터베이스 연결됨");
});
myEmitter.on("network:connection", () => {
console.log("네트워크 연결됨");
});
// 이벤트 발생
myEmitter.emit("database:connection");
Node.js 코어 모듈에서의 EventEmitter 사용 예
Node.js의 많은 코어 모듈들이 EventEmitter를 상속합니다. 예를 들어, fs.ReadStream
, http.Server
, net.Socket
등입니다:
const http = require("http");
// http.Server는 EventEmitter를 상속함
const server = http.createServer();
// 이벤트 리스너 등록
server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World\n");
});
server.on("connection", (socket) => {
console.log("새로운 연결이 생성되었습니다");
});
server.on("close", () => {
console.log("서버가 종료되었습니다");
});
// 서버 시작
server.listen(3000, () => {
console.log("서버가 포트 3000에서 실행 중입니다");
});
비동기 반복 처리를 위한 EventEmitter 활용
대규모 데이터셋을 비동기적으로 처리할 때 EventEmitter가 유용합니다:
const EventEmitter = require("events");
const fs = require("fs");
const readline = require("readline");
class LineProcessor extends EventEmitter {
processFile(filePath) {
const rl = readline.createInterface({
input: fs.createReadStream(filePath),
crlfDelay: Infinity,
});
let lineCount = 0;
rl.on("line", (line) => {
lineCount++;
// 각 라인에 대한 이벤트 발생
this.emit("line", line, lineCount);
});
rl.on("close", () => {
// 파일 처리 완료 이벤트
this.emit("end", lineCount);
});
return this;
}
}
// 사용 예
const processor = new LineProcessor();
processor
.on("line", (line, lineCount) => {
if (lineCount % 10000 === 0) {
console.log(`${lineCount}번째 라인 처리 중: ${line.substring(0, 50)}...`);
}
})
.on("end", (totalLines) => {
console.log(`파일 처리 완료. 총 ${totalLines}개 라인 처리됨`);
});
processor.processFile("large-data.csv");
요약
- EventEmitter는 Node.js의 이벤트 기반 아키텍처의 핵심으로, 이벤트를 발생시키고 처리하는 기능을 제공합니다.
- 주요 메소드:
on(event, listener)
: 이벤트 리스너 등록emit(event, [args])
: 이벤트 발생 및 리스너 호출once(event, listener)
: 한 번만 실행되는 리스너 등록removeListener(event, listener)
: 특정 리스너 제거removeAllListeners([event])
: 모든 리스너 제거
- 사용 패턴:
- 클래스 상속을 통한 이벤트 기능 구현
- 비동기 작업의 진행 상황 알림
- 컴포넌트 간 느슨한 결합 구현
- 주의사항:
- 메모리 누수 방지를 위한 리스너 관리
- 이벤트 이름 충돌 방지
EventEmitter는 Node.js의 비동기, 이벤트 기반 프로그래밍 모델의 핵심 요소로, 복잡한 비동기 흐름을 관리하고 컴포넌트 간의 통신을 효율적으로 구현하는 데 강력한 도구입니다.