자바스크립트는 싱글 스레드이다.
그런데 분명 여러 작업(task)들을 동시에 처리하는데 어떻게 하는걸까?
그 비밀은 아래 그림을 통해 알 수 있다.
위의 그림에서 Web API는 브라우저에서 제공하는 API들을 포괄하는 부분인데 브라우저마다 조금씩 다르지만, 크롬 브라우저 경우 Web API는 멀티 스레드로 구현되어 있다.
그럼 간단한 코드와 함께 예시를 들어보면 다음과 같이 진행된다.
setTimeout(() => {
console.log("3초 대기");
}, 3000);
console.log("1번");
console.log("2번");
setTimeout
함수가 Call Stack에 들어온다.console.log("1번");
이 실행.console.log("2번");
실행.이러한 방식으로 브라우저의 멀티스레드를 이용해 비동기 함수는 동시적 처리 작업이 가능해 지고 우리는 이를 통해 성능 향상을 누릴 수 있다.
브라우저의 WEB APIs와 Node.js 의 Node.js APIs는 구성은 비슷하다.
그러나 어떻게 Callback Queue에 비동기 작업이 들어가는지를 살펴보면 동작의 차이가 있다.
아래 코드를 보면 어떤 결과가 나올까?
console.log("시작");
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve("Promise")
.then(res => console.log(res));
console.log("끝");
왜 이런 결과가 나올까?
그 이유는 Callback Queue에는 2가지 종류가 있기 때문이다.
아래 코드의 예상 결과를 말해보자.
참고 - 최신 자바스크립트에서는 최상위에서 await를 사용할 수 있다.
const firstPromise = () => Promise.resolve("Promise");
const testFnc = async () => {
console.log("In Func");
const tmp = await firstPromise();
console.log(tmp);
}
console.log("시작");
await testFnc();
console.log("끝");
결론부터 말하면 다음과 같다.
시작
In Func
Promise
끝
왜 시작 - 끝 - In Fun - Promise
가 아닐까 ?
그 이유는 마지막 부분 코드가 결국 아래 코드와 같은 의미를 갖기 때문이다.
console.log("시작");
testFnc().then(() =>{
console.log("끝");
});
즉 await 키워드 다음에 오는 동일 라인의 코드는 then
핸들러의 콜백 함수로 들어간다는 뜻이다.
EventEmitter는 Node.js의 핵심 모듈 중 하나로, 이벤트 기반(Event-driven) 프로그래밍을 지원한다.
EventEmitter는 이벤트를 등록시키고 콜백 함수를 이용해 이벤트가 발생했을 때 콜백 함수를 실행한다.
이벤트 기반(Event-driven) 프로그래밍이란, 특정 이벤트가 발생할 때 해당 이벤트에 대해 정의된 콜백 함수를 실행하는 방식을 의미한다.
Node.js의 핵심 모듈과 클래스에서 EventEmitter를 상속받아서 동작한다.
이런 사례를 보면 EventEmitter를 왜 사용하는지, 그리고 EventEmitter의 강력함을 이해할 수 있을 것 같다.
fs 모듈의 FSWatcher 객체도 EventEmitter를 상속받아 파일 시스템 변경 이벤트를 감지한다.
let fs = require("fs");
const watcher = fs.watch('input.text', (eventType, filename) => {
console.log(`파일 변경 이벤트 발생: ${eventType} - ${filename}`);
});
watcher.on('change', (eventType, filename) => {
console.log(`change 이벤트: ${eventType} - ${filename}`);
console.log(watcher.__proto__);
});
아래 출력 결과를 보면 FSWatcher 모듈이 EventEmitter를 상속 받는것을 확실하게 알 수 있다.
출력 결과
Node.js의 http 모듈에서 Server 객체도 EventEmitter를 상속받아 다양한 이벤트를 발생시킨다.
예를 들어, 새로운 요청이 들어오면 request 이벤트를 발생시킨다.
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World\n');
});
server.on('request', (req, res) => {
console.log(`새로운 요청이 들어왔습니다: ${req.url}`);
});
server.listen(3000, () => {
console.log('서버가 실행 중입니다');
});
우리가 Node.js를 이용해 알고리즘을 풀면 자주 쓰게 되는 readline 모듈도 EventEmitter를 상속받아 입력을 비동기적으로 처리한다.
아래 코드는 우리가 자주 사용했을 readline 코드이다.
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.on('line', (line) => {
console.log(`입력된 내용: ${line}`);
rl.close();
});
rl.on('close', () => {
console.log('입력 종료');
process.exit();
});
그 외에도 Node.js의 pocess 객체도 그렇고 많은 모듈에서 EventEmitter를 사용하고 있다.
그런데 위의 readline 코드를 보면 이상한 부분이 있다.
첫번째 이벤트 등록에서 아직 등록되지 않은 "close" 이벤트를 호출하고 있다.
rl.on('line', (line) => {
console.log(`입력된 내용: ${line}`);
rl.close();
});
rl.on('close', () => {
console.log('입력 종료');
process.exit();
});
이 부분을 이해하기 위해서는 Node.js의 이벤트 처리 흐름을 하나씩 짚고 가면 된다.
이벤트 리스너 등록
rl.on('line', function(line) { ... })
와 rl.on('close', function() { ... })
를 호출하여 이벤트 리스너를 등록한다. 이 과정은 동기적으로 실행된다.입력 대기
readline
인터페이스는 사용자 입력을 기다린다. 이 대기 상태는 비동기적으로 이루어진다.사용자 입력 처리
readline
모듈은 line
이벤트를 발생시킨다. 이 이벤트는 입력된 데이터와 함께 이벤트 큐에 등록된다.line
이벤트 리스너 실행
line
이벤트를 처리할 준비가 되면, 이벤트 큐에서 line
이벤트를 가져와 등록된 리스너를 실행한다. 이 시점에서 line
이벤트 리스너가 호출되며, 입력 데이터가 파싱되어 필요한 작업이 수행된다.rl.close()
호출
line
이벤트 리스너 내에서 rl.close()
가 호출된다. 이 메서드는 readline
인터페이스를 종료시키며, close
이벤트를 발생시킨다. close
이벤트는 이벤트 큐에 등록된다.close
이벤트 리스너 실행
close
이벤트를 처리할 준비가 되면, 이벤트 큐에서 close
이벤트를 가져와 등록된 리스너를 실행한다.요약
Node 내부 구조 이미지
이미지 참조 : https://sjh836.tistory.com/149
Node.js의 이벤트 루프와 비동기 입출력 처리는 위의 사진에 보이는 libuv라는 C 라이브러리를 통해 이루어 진다.
이벤트 루프 관리
비동기 입출력 작업
타이머
setTimeout
과 setInterval
과 같은 타이머 기능을 제공하여 비동기적으로 타이머 콜백을 실행한다.이벤트 루프는 다음과 같은 주요 단계로 비동기 작업을 처리한다.
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
자료 참조 : https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick
setTimeout
과 setInterval
의 콜백을 실행.setImmediate
콜백을 실행.Node.js의 비동기 작업은 libuv를 통해 처리되며, 이벤트 루프는 이러한 작업의 완료를 감지하고 콜백을 실행한다.
예를 들어, 사용자가 입력을 하면 readline
모듈은 비동기적으로 입력을 대기하며, 입력이 완료되면 이벤트가 발생하고, 해당 이벤트에 대해 등록된 리스너가 호출된다.
https://medium.com/zigbang/nodejs-event-loop파헤치기-16e9290f2b30
https://inpa.tistory.com/entry/🔄-자바스크립트-이벤트-루프-구조-동작-원리#
https://inpa.tistory.com/entry/🌐-js-async#비동기의_병렬_처리_원리
https://ramincoding.tistory.com/entry/JavaScript-이벤트와-이벤트-핸들러-이벤트-객체-evente
https://kay0426.tistory.com/25
libuv 참고 자료
https://docs.libuv.org/en/v1.x/design.html