이 글은 Node.js를 더 잘 다루기 위해 비동기 제어를 설명합니다.
일부러 돌아가는 길을 선택함으로써 일반적으로 사용하지 않았을 법한 것을 설명합니다.
import axios from "axios";
async function getWebContent(url: string) {
const { data } = await axios.get(url);
return data;
}
(async () => {
await getWebContent("https://google.com");
})();
즉시 실행 함수 형태로 구글의 메인 페이지 문서를 읽는 코드를 작성했습니다.
간단합니다.
다음으로는, 이를 10번 반복하는 함수를 만들 것입니다.
import axios from "axios";
async function getWebContent(url: string) {
const { data } = await axios.get(url);
return data;
}
(async () => {
const urls = new Array(10).fill("https://google.com");
urls.forEach(async (el) => {
const google = await getWebContent(el);
console.log(google);
});
})();
운좋게도 Node.js는 모든 게 비동기적으로 동작합니다.
그래서 10번을 호출함에도 불구하고 1번을 호출하는 것과 거의 비슷한 시간 내에 실행됩니다.
하지만 10번을 불러오는 것을 성공했음에도, 여기에는 큰 문제가 있습니다.
바로 비동기적으로 호출된다는 점으로 인해, 실제 실행 순서가 의도와 다를 수 있다는 점입니다.
forEach문을 아래처럼 고쳐서, index를 찍어보도록 할까요?
import axios from "axios";
async function getWebContent(url: string) {
const { data } = await axios.get(url);
return data;
}
(async () => {
const urls = new Array(10).fill("https://google.com");
urls.forEach(async (el, i) => {
const google = await getWebContent(el);
console.log(i);
});
})();
저는 실행 결과 7, 6, 9, 3, 2, 0, 1, 4, 5, 8의 순서로 출력이 되었습니다.
일반적인 생각으로는 0부터 9까지 차례대로 나와야 하는데 말이죠.
한 번 순서가 제대로 나오도록 고쳐봅시다.
import axios from "axios";
// This "possibly" works in one of the Threads in a pool
async function getWebContent(url: string) {
const { data } = await axios.get(url);
return data;
}
(async () => {
const urls = new Array(10).fill("https://google.com");
for await (const url of urls) {
const google = await getWebContent(url);
console.log(google);
}
})();
for await를 사용함으로써 간단하게 고칠 수 있었습니다.
순서는 이제 보장이 될 것입니다.
하지만 동기적으로 고침으로써 오히려 성능은 10배 ( n번 만큼 ) 느려지고 말았습니다.
그러면 비동기의 성능을 가지면서 순서를 보장할 수는 없는 걸까요?
import axios from "axios";
async function getWebContent(url: string) {
const { data } = await axios.get(url);
return data;
}
(async () => {
const urls = new Array(10).fill("https://google.com");
const result = await Promise.all(urls.map((url) => getWebContent(url)));
result.forEach((el) => console.log(el));
})();
순서도 보장된 상태로 가져올 수 있습니다.
정확히 말하면, 동시에 보내기는 하되, 돌아온 것을 순서에 맞게 정렬해줬다고 할 수 있겠네요.
Promise.all이나 Promise.allSettled, 그 외에도 몇 가지 좋은 메서드들이 이미 존재합니다.
이로 인해서 성능과 순서를 보장할 수 있게 됐네요.
import { EventEmitter } from "events";
import axios from "axios";
const urls = new Array(10).fill("https://google.com");
const baseEvent = new EventEmitter();
const responses = [];
baseEvent.on("request", async (url) => {
const { data } = await axios(url);
baseEvent.emit("response", data);
});
baseEvent.on("response", (html) => {
responses.push(html);
if (responses.length === urls.length) {
baseEvent.emit("end", responses);
}
});
baseEvent.once("end", () => {
console.log(responses.length);
});
urls.map((url) => baseEvent.emit("request", url));
Node.js에서 제공하는 EventEmitter는 훌륭한 메서드를 가지고 있고,
우리는 이를 통해서 손쉽게 옵저버 패턴을 구현할 수 있습니다.
심지어 성능 면에서도 Promise보다 더 우수합니다.
import axios from "axios";
import { EventEmitter } from "events";
class MyEventEmitter extends EventEmitter {
constructor() {
super();
this.urls = [];
this.responses = [];
}
async getWebContent(url: string) {
const { data } = await axios.get(url);
this.responses.push(data);
if (this.urls.length === this.responses.length) {
this.emit("finish", this.responses);
}
}
addUrl(url) {
if (typeof url === "string") {
this.urls.push(url);
return this;
}
this.urls.push(...url);
return this;
}
work() {
this.urls.map((url, i) => {
this.emit("work", url);
});
}
}
console.time("emitter");
const urls = new Array(10).fill("https://google.com");
const myEventEmitter = new MyEventEmitter();
myEventEmitter
.addUrl(urls)
.on("work", (url) => myEventEmitter.getWebContent(url))
.on("finish", (responses) => console.timeEnd("emitter"))
.work();
또한 상속을 통해 클래스 형태로 만들 수도 있습니다.
여기까지로, 동기 비동기를 다루는 Node.js ( 엄밀히 말하면 비동기를 다루는 ) 방식을 배웠습니다.
사실 Node.js는 비동기 또는 non-blocking이라는 이름에서 알 수 있듯이,
비동기에 특화된 언어 ( 또는 엔진 ) 이라고 볼 수 있습니다.
따라서 Promise나 EventEmitter를 이용하면 그 특성을 더 잘 살릴 수 있을 것 같습니다.
Node.js 디자인 패턴 바이블 이라는 책에서 키워드를 얻었고,
이 글에서 영감을 얻어 작성한 글입니다.
for await와 Promise.all을 포함한 Promise 메서드들을 함께 보시면 더 좋을 거 같습니다.