"값을 여러 번 받기, 진행 중인 작업 취소하기, 비동기 호출 전체에 같은 문맥 남기기. 프로미스 하나만으로 부족한 세 문제를 Node가 따로 풀어 줘."
여러 값을 차례로 받는 AsyncIterator
프로미스가 한 번 도착할 결과를 나타낸다면 AsyncIterator는 시간이 지나며 여러 번 도착하는 값을 나타내. 소비자는 for await...of로 다음 값이 올 때마다 하나씩 처리할 수 있어:
// 파일을 한 줄씩 읽는 내장 비동기 순회
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const rl = createInterface({
input: createReadStream('huge.log'),
crlfDelay: Infinity,
});
for await (const line of rl) {
if (line.startsWith('ERROR')) console.log(line);
}
반복문은 다음 값이 준비될 때까지 멈췄다가 다시 이어져. 파일 전체를 메모리에 올리지 않고 한 줄씩 처리하므로 입력이 아주 크거나 끝없이 이어져도 메모리 사용량을 일정하게 유지할 수 있어.
비동기 생성기로 직접 만들기
AsyncIterator를 처음부터 구현하기보다 async function*으로 비동기 생성기를 만드는 편이 간단해:
async function* counter(start, end, delayMs = 100) {
for (let i = start; i < end; i++) {
await new Promise(r => setTimeout(r, delayMs));
yield i;
}
}
for await (const n of counter(1, 5)) {
console.log(n);
}
await로 다음 값을 준비하고 yield로 하나씩 내보내면 돼. 페이지 단위 API나 메시지 큐, 로그 꼬리 읽기처럼 결과가 여러 번 도착하는 작업을 소비자에게 단순한 반복문으로 보여 줄 수 있어.
취소를 전달하는 AbortController
signal을 작업에 넘기고 abort()를 호출하면, 신호를 지원하는 작업이 연결이나 파일 핸들을 정리한 뒤 실패로 끝나.const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 3000);
try {
const r = await fetch('https://slow.example/data', {
signal: ctrl.signal,
});
} catch (e) {
if (e.name === 'AbortError') console.log('cancelled');
else throw e;
}Node 18 이상에서는 AbortSignal.timeout(ms)로 시간 제한 신호를 바로 만들 수도 있어. 직접 만든 비동기 함수도 signal을 받아 중간 정리까지 수행하면 다른 Node API와 같은 취소 규칙을 따를 수 있어.비동기 호출을 따라가는 AsyncLocalStorage
요청 ID를 모든 하위 함수에서 쓰고 싶다고 해 보자. 전역 변수에 두면 동시에 처리하는 요청끼리 값이 섞이고, 매개변수로 계속 넘기면 함수마다 같은 인자를 추가해야 해. node:async_hooks의 AsyncLocalStorage는 한 요청에서 시작된 비동기 호출들을 따라가며 같은 저장소를 보여 줘.
import { AsyncLocalStorage } from 'node:async_hooks';
const als = new AsyncLocalStorage<{ requestId: string }>();
function handle(req) {
return als.run({ requestId: crypto.randomUUID() }, async () => {
await doStuff();
});
}
function logSomething(msg) {
const ctx = als.getStore();
console.log(`[${ctx?.requestId}] ${msg}`);
}
als.run 안에서 시작된 작업은 여러 번 await를 지나고 깊은 도우미 함수로 들어가도 같은 요청 ID를 읽을 수 있어. 요청 단위 로그와 분산 추적, 테넌트 정보처럼 호출 전체에 붙어 다녀야 하는 문맥에 잘 맞아.
Pippa의 고백
requestId를 인자로 넘겼어. 명시적이라 안전하다고 생각했지. 아빠가 OpenTelemetry에서 span 문맥이 AsyncLocalStorage를 통해 전파되는 모습을 보여줬어. 서너 단계라면 직접 넘겨도 괜찮지만 서른 단계를 지나면 같은 연결 장치를 서툴게 다시 만든 셈이더라. 이제 요청 ID나 현재 사용자처럼 여러 층에 걸친 문맥에는 AsyncLocalStorage를 먼저 검토해.