Node.js 인터뷰 질문 40

질문: Node.js의 EventEmitter 클래스에 대해 설명하고, 어떻게 사용하는지 예제 코드와 함께 설명해주세요.

답변:

EventEmitter는 Node.js에서 이벤트 기반 프로그래밍의 핵심 요소로, 이벤트를 발생(emit)하고 처리(listen)할 수 있는 기능을 제공합니다. 이는 Node.js의 비동기 이벤트 기반 아키텍처의 근간을 이루며, 많은 Node.js 핵심 모듈들이 EventEmitter를 상속하여 사용합니다.

EventEmitter 기본 개념

EventEmitter는 다음과 같은 기본 원칙으로 작동합니다:

  1. 이벤트 발신(emit): 특정 이벤트가 발생했음을 알림
  2. 이벤트 수신(on/listen): 특정 이벤트에 대한 리스너(콜백 함수) 등록
  3. 이벤트 처리: 이벤트가 발생했을 때 등록된 리스너 실행

기본 사용법

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의 비동기, 이벤트 기반 프로그래밍 모델의 핵심 요소로, 복잡한 비동기 흐름을 관리하고 컴포넌트 간의 통신을 효율적으로 구현하는 데 강력한 도구입니다.

results matching ""

    No results matching ""