이 글은 발표한 내용을 정리한 것입니다.
Node.js에서 기본적으로 사용되고 중요한 또 다른 패턴은 관찰자 패턴입니다. 리액터 ( Reactor )그리고 콜백 ( Callback ) 과 함께 관찰자 패턴은 비동기적인 Node.js 세계를 숙달하는 데 필수적인 조건입니다. 관찰자 패턴은 Node.js의 반응적 (reactive) 특성을 모델링하고 콜백을 완벽하게 보완하는 이상적인 해결책입니다. 다음과 같이 공식적인 정의를 내릴 수 있습니다.
관찰자 패턴은 상태 변화가 일어날 때 관찰자 ( 또는 listener )에게 통지할 수 있는 객체를 정의하는 것입니다.
설명만 들어도 벅차오르네요.
요약하면 콜백 패턴을 보완하는 이상적인 형태라는 것입니다. 콜백은 하나의 리스너를 갖는 반면, 관찰자 패턴은 실질적으로 여러 관찰자를 지정할 수 있으니깐요.
const { EventEmitter } = require("events");
const emitter = new EventEmitter();
이것만으로도 바로 Observer를 만들 수 있습니다!
EventEmitter의 필수 메서드를 보도록 하겠습니다.
const { EventEmitter } = require("events");
const { readFile } = require("fs");
function findRegex(files, regex) {
const emitter = new EventEmitter();
for (const file of files) {
readFile(file, "utf8", (err, content) => {
if (err) {
return emitter.emit("error", err);
}
emitter.emit("fileread", file, content);
const match = content.match(regex);
if (match) {
match.forEach((el) => emitter.emit("found", file, el));
}
});
}
return emitter;
}
findRegex(["fileA.txt", "fileB.json"], /hello \w+/g)
.on("fileread", (file) => console.log(`${file} was read.`))
.on("found", (file, match) => console.log(`Matched ${match} in ${file}`))
.on("error", (err) => console.error(`Error emitted ${err.message}`));
const { EventEmitter } = require("events");
const { readFile } = require("fs");
const log = console.log;
class FindRegex extends EventEmitter {
constructor(regex) {
super();
this.regex = regex;
this.files = [];
}
addFile(file) {
this.files.push(file);
return this;
}
find() {
for (const file of this.files) {
readFile(file, "utf8", (err, content) => {
if (err) {
return this.emit("error", err);
}
this.emit("fileread", file);
const match = content.match(this.regex);
if (match) {
match.forEach((el) => this.emit("found", file, el));
}
});
return this;
}
}
}
const findRegexInstance = new FindRegex(/hello \w+/g)
.addFile("fileA.txt")
.addFile("fileB.json")
.find()
.on("found", (file, match) => log(`Matched "${match}" in file ${file}.`))
.on("error", (err) => console.error(`Error emitted ${err.message}`));
같은 코드인데, 하나는 클래스 형태로 작성된 예제에요.
사실 이 코드는, 동기적이라고 가정할 경우에는 제대로 동작할 거라고 볼 수 없어요.
왜냐하면 find를 실행한 다음에, 이벤트를 등록하고 있기 때문이에요.
하지만 비동기적이라고 한다면,
지금 현 스택의 코드가 동작한 다음에, 이벤트 루프가 넘어가면서 동작할 거란 걸 알 수 있죠.
그래서 이 코드들은 정상적인 동작을 보장받고 있습니다.
findRegexInstance
.addFile(...)
.find() // find 이후에 이벤트를 등록함에도 정상적으로 동작하는 이유를 위에 설명했다.
.on('found', ...)
사실 둘은 기능적으로는 같습니다.
둘의 사용 시점을 구분하기 위해서는 가독성, 의미, 코드의 양을 생각하는 게 낫습니다.
콜백은 여러 유형의 결과를 전달할 수 없고, 단 한 번만 실행됩니다.
또 여러 개의 콜백을 둬서 각 이벤트마다 따로 처리해야 한다면 EventEmitter가 낫습니다.
여러 번 발생하거나 혹은 발생하지 않을 수도 있는 이벤트인 경우에도 EventEmitter가 낫고요.