원문: https://exploringjs.com/nodejs-shell-scripting/ch_nodejs-overview.html!
4.1 Node.js 플랫폼
4.2 Node.js 이벤트 루프
4.3 libuv: Node.js용 비동기 I/O 등을 처리하는 크로스 플랫폼 라이브러리
4.4 사용자 코드로 메인 스레드 탈출하기
4.5 이 챕터의 출처
이 챕터에서는 Node.js의 작동 방식을 간략히 설명합니다.
아래 다이어그램은 Node.js의 전체적인 구조를 보여줍니다.
Node.js 앱에서 사용할 수 있는 API는 다음과 같이 구성됩니다.
fetch
및 CompressionStream
과 같은 크로스 플랫폼 웹 API가 이 범주에 속합니다.process
)'node:path'
(파일 시스템 경로를 처리하기 위한 함수 및 상수) 및 'node:fs'
(파일 시스템과 관련된 기능)를 통해 제공됩니다.Node.js API는 부분적으로 자바스크립트로 구현되며 일부는 C++로 구현됩니다. 후자는 운영 체제와 인터페이스하는 데 필요합니다.
Node.js는 내장된 V8 자바스크립트 엔진(Google의 Chrome 브라우저에서 사용되는 엔진과 동일)을 통해 자바스크립트를 실행합니다.
다음은 Node의 전역 변수에 대한 몇 가지 주요 내용입니다.
crypto
는 웹과 호환되는 crypto API에 대한 액세스를 제공합니다.console
은 브라우저의 동일한 전역 변수(console.log()
등)와 많이 겹칩니다.fetch()
는 Fetch broswer API를 사용할 수 있게 해줍니다.process
에는 process
class의 인스턴스가 포함되어 있으며 커맨드 라인 매개변수, 표준 입력, 표준 출력 등에 접근할 수 있습니다.structuredClone()
은 개체 복제를 위한 브라우저와 호환되는 함수입니다.URL
은 URL을 처리하기 위한 클래스로 브라우저와 호환됩니다.뿐만 아니라 이 장 전체에서 더 많은 전역 변수에 대해 설명하도록 하겠습니다.
다음 내장 모듈은 전역 변수에 대한 대안을 제공합니다.
'node:console'
은 전역 변수 console
의 대안입니다.console.log("Hello!");
import { log } from "node:console";
log("Hello!");
'node:process'
는 전역 변수 process
의 대안입니다:console.log(process.argv);
import { argv } from "node:process";
console.log(process.argv);
원칙적으로 모듈을 사용하는 것이 전역 변수를 사용하는 것보다 깔끔합니다. 그러나 전역 변수 console
과 process
를 사용하는 것이 널리 사용되는 일반적인 패턴이므로, 모듈을 사용하는 방법은 일반적인 패턴에서 벗어난다는 단점이 있습니다.
대부분의 Node API는 모듈을 통해 제공됩니다. 아래는 자주 사용되는 몇 가지 항목입니다(알파벳순)
'node:assert/strict'
: Assertion은 조건이 충족되었는지 확인하고, 충족되지 않은 경우 오류를 보고하는 함수입니다. 애플리케이션 코드 및 단위 테스트에 사용할 수 있습니다. 다음은 이 API를 사용하는 예입니다.import * as assert from 'node:assert/strict';
assert.equal(3 + 4, 7);
assert.equal('abc'.toUpperCase(), 'ABC');
assert.deepEqual({prop: true}, {prop: true}); // 깊은 비교
assert.notEqual({prop: true}, {prop: true}); // 얕은 비교
'node:child_process'
는 네이티브 명령을 동기적으로 또는 별도의 프로세스에서 실행하기 위한 것입니다. 이 모듈은 §12 "하위 프로세스에서 쉘 명령 실행"에 설명되어 있습니다.
'node:fs'
는 파일 및 디렉토리 읽기, 쓰기, 복사 및 삭제와 같은 파일 시스템 작업을 제공합니다. 자세한 내용은 §8 "Node.js에서 파일 시스템 작업"을 참조하세요.
'node:os'
는 운영 체제별 상수와 유틸리티 함수가 포함되어 있습니다. 그 중 일부는 §7 "Node.js에서 파일 시스템 경로 및 파일 URL 작업"에 설명되어 있습니다.
'node:path'
파일 시스템 경로 작업을 위한 크로스 플랫폼 API입니다. §7 "Node.js에서 파일 시스템 경로 및 파일 URL 사용"에 설명되어 있습니다.
'node:stream'
에는 §9 "네이티브 Node.js 스트림"에 설명된 Node.js 전용 스트림 API가 포함되어 있습니다.
Node.js는 §10 "Node.js에서 웹 스트림 사용"의 주제인 크로스 플랫폼 웹 스트림 API도 지원합니다.
'node:util'
에는 다양한 유틸리티 기능이 포함되어 있습니다.
모듈 'node:module'
에는 모든 내장 모듈의 지정자가 있는 배열을 반환하는 함수 'built in Modules()'
가 포함되어 있습니다.
import * as assert from 'node:assert/strict';
import {builtinModules} from 'node:module';
// 내부 모듈 제거(이름이 밑줄로 시작하는 모듈)
const modules = builtinModules.filter(m => !m.startsWith('\_'));
modules.sort();
assert.deepEqual(
modules.slice(0, 5),
[
'assert',
'assert/strict',
'async_hooks',
'buffer',
'child_process',
]
);
이 섹션에서는, 다음과 같은 import를 사용합니다
import * as fs from "node:fs";
Node의 함수는 세 가지 스타일로 제공됩니다. 내장 모듈 'node:fs'
를 예로 살펴보겠습니다.
앞서 살펴본 세 가지 예시에서는 유사한 기능을 가진 함수들에 대한 명명 규칙을 볼 수 있습니다.
fs.readFile()
입니다.fsPromises.readFile()
fs.readFileSync()
이 세 가지 스타일이 어떻게 동작하는지 자세히 살펴보겠습니다.
동기 함수는 굉장히 간단합니다. 즉시 값을 반환하고 예외로 오류를 발생시킵니다.
try {
const result = fs.readFileSync("/etc/passwd", { encoding: "utf-8" });
console.log(result);
} catch (err) {
console.error(err);
}
Promise 기반 함수는 이행 상태는 결과와 함께 충족(fulfilled)되거나 에러와 함께 거부(rejected)되는 Promise를 리턴합니다.
import * as fsPromises from "node:fs/promises"; // (A)
try {
const result = await fsPromises.readFile("/etc/passwd", {
encoding: "utf-8",
});
console.log(result);
} catch (err) {
console.error(err);
}
A행의 모듈 지정자를 참고하세요. Promise 기반 API는 다른 모듈에 있습니다.
Promise는 "성급한 프로그래머를 위한 자바스크립트"에 더 자세히 설명되어 있습니다.
콜백 기반 함수는 결과와 에러를 마지막 매개변수인 콜백에 전달합니다.
fs.readFile("/etc/passwd", { encoding: "utf-8" }, (err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result);
});
이 스타일은 Node.js 문서에 자세히 설명되어 있습니다.
기본적으로 Node.js는 모든 자바스크립트를 싱글 스레드(메인 스레드)에서 실행합니다. 메인 스레드는 이벤트 루프
(자바스크립트 청크를 실행하는 루프)를 지속적으로 실행합니다. 각 청크는 콜백이며 협조적으로 예약된 태스크로 간주될 수 있습니다. 첫 번째 태스크에는 Node.js를 시작하는 코드(모듈 또는 표준 입력에서 오는 코드)가 포함되어 있습니다. 다른 태스크들은 보통 다음과 같은 이유로 나중에 추가됩니다.
이벤트 루프의 대략적인 첫 모습은 다음과 같습니다.
즉, 메인 스레드는 다음과 유사한 코드를 실행합니다.
while (true) {
// 이벤트 루프
const task = taskQueue.dequeue(); // 블록들
task();
}
이벤트 루프는 태스크 큐에서 콜백을 가져와 메인 스레드에서 실행합니다. 태스크 큐(task queue
)가 비어 있는 경우 기본 스레드를 중단(block
)합니다.
다음 두 가지 주제에 대해서는 나중에 살펴보겠습니다.
이 루프를 이벤트 루프라고 하는 이유는 무엇일까요? 이벤트에 대한 응답으로 많은 태스크가 추가되기 때문입니다. 예를 들어 입력 입력 데이터를 처리할 준비가 되었을 때 운영 체제에서 보낸 이벤트들이 있습니다.
콜백은 태스크 큐에 어떻게 추가될까요? 다음은 흔하게 가능성이 있는 경우입니다.
event emitter
(이벤트 소스)가 이벤트를 발생시키면 이벤트 리스너의 호출이 태스크 큐에 추가됩니다.import * as fs from "node:fs";
function handleResult(err, result) {
if (err) {
console.error(err);
return;
}
console.log(result); // (A)
}
fs.readFile("reminder.txt", "utf-8", handleResult);
console.log("AFTER"); // (B)
출력은 다음과 같습니다.
AFTER
Don’t forget!
fs.readFile()
은 다른 스레드에서 파일을 읽는 코드를 실행합니다. 이 경우 코드 실행은 성공하고 이 콜백을 태스크 큐에 추가합니다.
() => handleResult(null, "Don’t forget!");
Node.js가 자바스크립트 코드를 실행하는 방법에 대한 중요한 규칙은 다음과 같습니다. 각 태스크는 다른 태스크가 실행되기 전에 완료("완료될 때까지 실행")됩니다. 이전 예제에서 알 수 있습니다. 줄 B의 'AFTER'
는 결과가 줄 A에 기록되기 전에 기록됩니다. 이는 handleResult()
호출로 태스크가 실행되기 전에 초기 태스크가 완료되기 때문입니다.
완료까지 실행한다는 것은 태스크의 수명이 겹치지 않기 때문에, 백그라운드에서 공유 데이터가 변경되는 것에 대해 걱정할 필요가 없다는 것을 의미합니다. 그래서 Node.js 코드가 간소화됩니다. 다음 예는 그것을 보여줍니다. 간단한 HTTP 서버를 구현합니다.
// server.mjs
import * as http from "node:http";
let requestCount = 1;
const server = http.createServer((_req, res) => {
// (A)
res.writeHead(200);
res.end("This is request number " + requestCount); // (B)
requestCount++; // (C)
});
server.listen(8080);
node server.mjs
를 통해 이 코드를 실행합니다. 코드가 시작되고 HTTP 요청을 기다립니다. 이제 웹 브라우저를 사용하여 http://localhost:8080
으로 이동할 수 있습니다. 해당 HTTP 리소스를 다시 로드할 때마다 Node.js는 A 에서 시작하는 콜백을 호출합니다. 현재 값인 변수 requestCount(줄 B)가 포함된 메시지를 제공하고 이를 증가시킵니다(줄 C).
콜백의 각 호출은 새로운 태스크이며 변수 requestCount는 태스크 간에 공유됩니다. 실행이 완료되기 때문에 읽고 업데이트하기 쉽습니다. 동시에 실행중인 다른 태스크들이 없기 때문에, 다른 태스크과 동기화할 필요가 없습니다.
Node.js 코드가 기본적으로 단일 스레드(이벤트 루프 포함)에서 실행되는 이유는 무엇일까요? 여기에는 두 가지 이점이 있습니다.
이미 살펴본 바와 같이, 스레드가 하나만 있는 경우 태스크 간에 데이터를 공유하는 것이 더 간단합니다.
기존의 다중 스레드 코드에서는 완료까지 가장 오래 걸리는 태스크가 끝날 때까지 현재 스레드를 차단합니다. 이러한 태스크 예시로는 파일 읽기 또는 HTTP 요청 처리가 있습니다. 매번 스레드를 새로 만들어야 하기 때문에, 이런 태스크들을 많이 수행하는 것은 비용이 많이 듭니다. 이벤트 루프를 사용하면 태스크 당 비용이 더 저렴합니다. 특히 각 태스크가 별 효과를 거두지 못하는 경우에는 더욱 그렇습니다. 이것이 이벤트 루프 기반 웹 서버가 스레드 기반 웹 서버보다 높은 로드를 처리할 수 있는 이유입니다.
Node의 비동기 테스크 중 일부가 메인 스레드가 아닌 다른 스레드에서 실행되고 태스크 큐를 통해 자바스크립트로 다시 보고된다는 점을 고려할 때, Node.js는 실제로 단일 스레드가 아닙니다. 대신 단일 스레드를 사용하여 (메인 스레드에서) 동시에 비동기적으로 실행되는 태스크를 조정합니다.
이것으로 이벤트 루프에 대한 첫 번째 고찰을 마칩니다. 피상적인 설명으로 충분하다면 이 섹션의 나머지 부분을 생략해도 됩니다. 자세한 내용은 계속 읽어보세요.
실제 이벤트 루프에는 여러 단계에서 읽는 여러 태스크 큐가 있습니다(GitHub 저장소 nodejs/node
에서 일부 자바스크립트 코드를 확인 가능). 다음 다이어그램은 이러한 단계 중 가장 중요한 단계를 보여줍니다.
다이어그램에 표시된 이벤트 루프 단계는 무엇을 할까요?
"timers" 단계는 다음을 통해 큐에 추가된 시간이 지정된 태스크(timed tasks
)를 호출합니다.
setTimeout(task, delay=1)
은 밀리세컨드 지연(delay
) 후 콜백 태스크(task
)를 실행합니다.setInterval(task, delay=1)
콜백 태스크(task
)를 반복적으로 실행하고, 밀리세컨드 동안 지연(delay
)하는 일시 중지가 지속됩니다."poll" 단계는 I/O 이벤트를 검색 및 처리하고 큐에서 I/O 관련 태스크를 실행합니다.
"check" 단계("즉시 단계")는 다음을 통해 예약된 태스크를 실행합니다.
setImmediate(task)
는 가능한 한 빨리 콜백 태스크를 실행합니다("poll" 단계 후 "immediately").각 단계는 큐가 비어 있거나 최대 수의 태스크가 처리될 때까지 실행됩니다. "poll"을 제외한 각 단계는 실행 중에 추가된 태스크를 처리하기 전에 다음 차례까지 대기합니다.
폴링 큐가 비어 있지 않으면, 폴링 단계는 해당 큐를 통과하고 해당 태스크를 실행합니다.
폴링 큐가 비어 있으면 다음을 수행합니다.
setImediate()
태스크가 있는 경우, 처리가 "check" 단계로 진행됩니다.이 단계가 시스템 종속 시간 제한보다 오래 걸리는 경우, 이 단계는 종료되고 다음 단계가 실행됩니다.
각 태스크가 호출된 후,두 단계로 구성된 "하위 루프(sub-loop)"가 실행됩니다.
하위 단계는 다음을 처리합니다.
process.nextTick()
을 통해 큐에 추가된 Next-tick 태스크queueMicrotask()
, Promise 리액션 등을 통해 큐에 추가된 마이크로 태스크Next-tick 태스크는 Node.js 전용이며 마이크로 태스크는 크로스 플랫폼 웹 표준입니다(MDN의 지원 표 참조)
이 하위 루프는 두 큐가 모두 비워질 때까지 실행됩니다. 실행 중에 추가된 태스크는 즉시 처리됩니다. 실행 중에 추가된 태스크는 즉시 처리되며, 하위 루프는 다음 차례까지 기다리지 않습니다.
다음 기능 및 방법을 사용하여 태스크 큐 중 하나에 콜백을 추가할 수 있습니다.
setTimeout()
(웹 표준)setInterval()
(웹 표준)setImmediate()
(Node.js 전용)process.nextTick()
(Node.js 전용)queueMicrotask()
: (웹 표준)지연을 통해 태스크 타이밍을 맞출 때 태스크 실행이 가능한 가장 빠른 시간을 지정하는 것이 중요합니다. Node.js는 예정된 태스크가 있는 경우에만 태스크 사이를 확인할 수 있기 때문에, 정확히 예약된 시간에 실행할 수 없습니다. 따라서 태스크가 오래 실행되면 시간이 지정된 태스크가 지연될 수 있습니다.
아래 코드를 확인해보세요.
function enqueueTasks() {
Promise.resolve().then(() => console.log('Promise reaction 1'));
queueMicrotask(() => console.log('queueMicrotask 1'));
process.nextTick(() => console.log('nextTick 1'));
setImmediate(() => console.log('setImmediate 1')); // (A)
setTimeout(() => console.log('setTimeout 1'), 0);
Promise.resolve().then(() => console.log('Promise reaction 2'));
queueMicrotask(() => console.log('queueMicrotask 2'));
process.nextTick(() => console.log('nextTick 2'));
setImmediate(() => console.log('setImmediate 2')); // (B)
setTimeout(() => console.log('setTimeout 2'), 0);
}
setImmediate(enqueueTasks);
ESM 모듈의 특수성을 피하기 위해 setImmediate()
를 사용합니다. 이는 마이크로 태스크에서 실행됩니다. 즉, ESM 모듈의 최상위 레벨에서 마이크로 태스크를 큐에 넣으면 next-tick 태스크 전에 실행됩니다. 이후에 보게 되겠지만 대부분의 다른 상황에서는 다릅니다.
다음은 이전 코드의 출력입니다.
nextTick 1
nextTick 2
Promise reaction 1
queueMicrotask 1
Promise reaction 2
queueMicrotask 2
setTimeout 1
setTimeout 2
setImmediate 1
setImmediate 2
살펴본 결과
모든 next-tick 태스크는 enqueueTasks()
직후에 실행됩니다.
Promise 리액션을 포함한 모든 마이크로 태스크가 뒤따릅니다.
"timer" 단계는 immediate 단계 바로 다음에 옵니다. 이 때 시간 지정된 태스크를 실행됩니다.
immediate ("check") 단계(줄 A 및 줄 B) 동안 immediate 태스크를 추가했습니다. 출력에서 마지막으로 표시됩니다. 즉, 현재 단계에서 실행되지 않고 다음 immediate 단계에서 실행되었습니다.
다음 코드는 next-tick 단계에서 next-tick 태스크를 큐에 넣고, 마이크로 태스크 단계에서 마이크로 태스크를 큐에 넣으면 어떻게 되는지 살펴봅니다.
setImmediate(() => {
setImmediate(() => console.log('setImmediate 1'));
setTimeout(() => console.log('setTimeout 1'), 0);
process.nextTick(() => {
console.log('nextTick 1');
process.nextTick(() => console.log('nextTick 2'));
});
queueMicrotask(() => {
console.log('queueMicrotask 1');
queueMicrotask(() => console.log('queueMicrotask 2'));
process.nextTick(() => console.log('nextTick 3'));
});
});
출력은 다음과 같습니다.
nextTick 1
nextTick 2
queueMicrotask 1
queueMicrotask 2
nextTick 3
setTimeout 1
setImmediate 1
살펴본 결과
next-tick 태스크가 먼저 실행됩니다.
"nextTick 2" 는 next-tick 단계에서 큐에 추가되고 즉시 실행됩니다. next-tick 큐가 비어 있는 경우에만 실행이 계속됩니다.
마이크로 태스크도 마찬가지입니다.
마이크로 태스크 단계에서 "nextTick 3"을 큐에 추가하고 실행 루프를 next-tick 단계로 되돌립니다. 이러한 하위 단계는 두 큐가 모두 비워질 때까지 반복됩니다. 그래야 실행이 다음 global 단계로 이동합니다. 먼저 "timers" 단계("setTimeout 1")를 수행하고, immediate 단계("setImmediate 1")를 수행합니다.
다음 코드는 이벤트 루프 단계를 중단시킬 수 있는 태스크 종류를 탐색합니다(무한 재귀를 통해 실행되지 않도록 방지)
import * as fs from "node:fs/promises";
function timers() {
// OK
setTimeout(() => timers(), 0);
}
function immediate() {
// OK
setImmediate(() => immediate());
}
function nextTick() {
// I/O 중단 시키기
process.nextTick(() => nextTick());
}
function microtasks() {
// I/O 중단 시키기
queueMicrotask(() => microtasks());
}
timers();
console.log("AFTER"); // 항상 로깅
console.log(await fs.readFile("./file.txt", "utf-8"));
"timers" 단계와 immediate 단계는 해당 단계에서 큐에 있는 태스크를 실행하지 않습니다. 그것이 timers()
와 immediate()
가 "poll" 단계에서 다시 전달하는 fs.readFile()
가 중단되지 않는 이유입니다(Promise 리액션도 있지만 여기서는 무시합시다)
next-tick 태스크와 마이크로 태스크가 예약되는 방식 때문에 nextTick()
과 microtasks()
모두 마지막 줄의 출력을 차단합니다.
이벤트 루프의 각 반복이 끝날 때 Node.js는 종료할 시간인지 확인합니다. 미해결된 시간 지정된 태스크에 대한 timeout
카운트를 유지합니다.
setImmediate()
, setInterval()
또는 setTimeout()
을 통해 시간이 지정된 태스크를 예약하면 참조 횟수가 늘어납니다.만약 이벤트 루프 반복이 끝날 때 참조 횟수가 0이면 Node.js가 종료됩니다.
다음 예시에서 확인할 수 있습니다.
function timeout(ms) {
return new Promise((resolve, _reject) => {
setTimeout(resolve, ms); // (A)
});
}
await timeout(3_000);
Node.js는 timeout()
에 의해 반환된 약속이 이행될 때까지 기다립니다. 이유가 뭘까요? A 줄에서 예약한 태스크가 이벤트 루프를 활성 상태로 유지하기 때문입니다.
반면에 Promise를 생성해도 참조 횟수는 증가하지 않습니다.
function foreverPending() {
return new Promise((_resolve, _reject) => {});
}
await foreverPending(); // (A)
이 경우 실행은 A줄에서 대기하는 동안 일시적으로 이 (메인) 태스크를 떠납니다. 이벤트 루프가 끝나면 참조 횟수는 0이고 Node.js가 종료됩니다. 그러나 종료는 성공하지 못합니다. 즉, 종료 코드는 0이 아니라 13입니다("Unfinished Top-Level Await").
타임아웃이 이벤트 루프를 활성 상태로 유지할지 여부를 수동으로 제어할 수 있습니다. 기본적으로 setImmediate()
, setInterval()
및 setTimeout()
을 통해 예약된 태스크가 미해결 상태인 경우 이벤트 루프를 활성 상태로 유지합니다. 이 함수는 .unref()
메서드가 기본값을 변경하는 Timeout
클래스의 인스턴스를 반환함으로서, 타임아웃이 활성화되어 있어도 Node.js가 종료되지 않도록 합니다. 메소드 .ref()
는 기본값을 복원합니다.
Tim Perry는 .unref()
사용 사례를 언급합니다. 그의 라이브러리는 백그라운드 태스크를 반복적으로 실행하기 위해 setInterval()
을 사용했습니다. 이 태스크로 인해 애플리케이션을 종료할 수 없었습니다. 그는 .unref()
를 통해 문제를 해결했습니다.
libuv는 많은 플랫폼(Windows, macOS, Linux 등)을 지원하는 C로 작성된 라이브러리입니다. Node.js는 이를 사용하여 I/O 등을 처리합니다.
네트워크 I/O는 비동기식이며, 현재 스레드를 차단하지 않습니다. 이러한 I/O에는 다음이 포함됩니다.
비동기 I/O를 처리하기 위해 libuv는 네이티브 커널 API를 사용하고 I/O 이벤트(Linux의 epoll, macOS를 포함한 BSD Unix의 kqueue, SunOS의 이벤트 포트, Windows의 IOCP)를 구독합니다. 그런 다음 알림이 발생할 때 알림을 받습니다. I/O 자체를 포함한 이러한 모든 활동은 메인 스레드에서 발생합니다.
파일 I/O 및 일부 DNS 서비스와 같은 일부 기본 I/O API가 차단(비동기 아님)됩니다. libuv는 스레드 풀(소위 "워커 풀")의 스레드에서 이러한 API를 호출합니다. 이를 통해 메인 스레드가 이러한 API를 비동기적으로 사용할 수 있습니다.
libuv는 Node.js가 I/O 뿐만 아니라 그 이상의 기능을 제공합니다. 기타 기능은 다음과 같습니다.
libuv는 GitHub 저장소 libuv/libuv
(uv_run() 함수)에서 소스 코드를 확인할 수 있는 자체 이벤트 루프를 가지고 있습니다.
Node.js가 I/O에 반응하도록 유지하려면 메인 스레드 태스크에서 장시간 실행되는 계산을 수행하지 않아야 합니다. 이를 위한 두 가지 옵션이 있습니다.
setImmediate()
를 통해 각 조각을 실행할 수 있습니다. 이를 통해 이벤트 루프가 조각 간에 I/O를 수행할 수 있습니다.다음 하위 섹션에서는 오프로드에 대한 몇 가지 옵션에 대해 설명합니다.
워커 스레드는 다음과 같이 몇 가지 차이점이 있는 크로스 플랫폼 웹 Workers API를 구현합니다.
모듈에서 워커 스레드를 가져와야 하며, 웹 워커(Web Worker)는 전역 변수를 통해 액세스됩니다.
워커 내에서 메시지를 듣고 게시하는 태스크는 브라우저에서 전역 객체의 방법을 통해 수행됩니다. Node.js에서는 parentPort를 대신 가져옵니다.
워커의 대부분의 Node.js API를 사용할 수 있습니다. 브라우저에서는 선택이 더 제한적(DOM 등을 사용할 수 없음)입니다.
Node.js에서는 브라우저보다 더 많은 개체(클래스가 내부 클래스 JSTransferable
을 확장하는 모든 개체)를 전송할 수 있습니다.
한편, 워커 스레드는 실제 스레드입니다. 프로세스보다 가볍고 메인 스레드와 동일한 프로세스로 실행됩니다.
Atomics
는 SharedArrayBuffers를 사용할 때 도움이 되는 원자 연산 및 동기화 프리미티브를 제공합니다.자세한 내용은 워커 스레드에 대한 Node.js 문서를 참조하십시오.
클러스터는 Node.js 전용 API입니다. 이를 통해 워크로드를 분산하는 데 사용할 수 있는 Node.js 프로세스 클러스터를 실행할 수 있습니다. 프로세스는 완전히 분리되지만 서버 포트를 공유합니다. 채널을 통해 JSON 데이터를 전달하여 통신할 수 있습니다.
프로세스 분리가 필요하지 않은 경우, 보다 가벼운 워커 스레드를 사용할 수 있습니다.
자식 프로세스는 또 다른 Node.js 전용 API입니다. 이를 통해 네이티브 명령을 실행하는 새로운 프로세스를 생성할 수 있습니다(종종 네이티브 셸을 통해). 이 API는 §12 "자식 프로세스에서 셸 명령 실행하기"에서 다룹니다.
Node.js 이벤트 루프:
process.nextTick()
"이벤트 루프에 대한 비디오(이 장에 필요한 일부 배경 지식을 새로 고침):
libuv:
자바스크립트 동시성: