| 일 | 월 | 화 | 수 | 목 | 금 | 토 | 
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
| 9 | 10 | 11 | 12 | 13 | 14 | 15 | 
| 16 | 17 | 18 | 19 | 20 | 21 | 22 | 
| 23 | 24 | 25 | 26 | 27 | 28 | 29 | 
| 30 | 
- 레벨 1
- 프로그래머스 레벨 2
- 소켓
- 프로그래머스
- HTTP 완벽 가이드
- 크롤링
- ip
- 수학
- socket
- TCP
- 알고리즘
- javascript
- 그래프
- HTTP
- Algorithm
- Nestjs
- 가천대
- 타입스크립트
- dfs
- 백준
- dp
- 문자열
- typescript
- Node.js
- 자바스크립트
- 쉬운 문제
- type challenge
- Crawling
- BFS
- 타입 챌린지
- Today
- Total
kakasoo
Node.js에서 동시성 다루기와 예제 본문

이 글은 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번 반복하는 함수를 만들 것입니다.
비동기적으로 구글 페이지 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까지 차례대로 나와야 하는데 말이죠.
한 번 순서가 제대로 나오도록 고쳐봅시다.
동기적으로 구글 페이지 10번 가져오기
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, 그 외에도 몇 가지 좋은 메서드들이 이미 존재합니다.
이로 인해서 성능과 순서를 보장할 수 있게 됐네요.
EventEmitter를 이용한 비동기 처리
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 메서드들을 함께 보시면 더 좋을 거 같습니다.
'프로그래밍 > Backend' 카테고리의 다른 글
| 부하 대응 접근 방식 (0) | 2022.09.12 | 
|---|---|
| 커머스에서의 장바구니 금액 계산법 (0) | 2022.08.27 | 
| 성능 기술과 백분위 ( percentile ) (0) | 2022.07.02 | 
| 백엔드 개발자가 블록체인을 배워야 할까? (0) | 2022.06.26 | 
| 확장성의 의미와 트위터의 사례 (0) | 2022.06.19 | 
 
                   
                  