본문 바로가기
C.W.K.
Stream
Lesson 05 of 05 · published

AsyncIterator, AbortController, AsyncLocalStorage

~14 min · async, async-iterator, abort-controller, async-local-storage

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"값을 여러 번 받기, 진행 중인 작업 취소하기, 비동기 호출 전체에 같은 문맥 남기기. 프로미스 하나만으로 부족한 세 문제를 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

AbortController는 Node와 Web API에서 비동기 작업에 취소 요청을 전달하는 표준 방식이야. 제어기에서 만든 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_hooksAsyncLocalStorage는 한 요청에서 시작된 비동기 호출들을 따라가며 같은 저장소를 보여 줘.

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를 먼저 검토해.

Code

페이지 단위 API를 비동기 반복자로 감싸기·javascript
// Build a paginated API consumer as an async iterator
async function* paginatedFetch(url) {
  let next = url;
  while (next) {
    const r = await fetch(next);
    const { data, nextPage } = await r.json();
    for (const item of data) {
      yield item;
    }
    next = nextPage;
  }
}

// Consumer never sees pagination — they see an infinite-ish stream
for await (const item of paginatedFetch('/api/items?page=1')) {
  if (item.id === target) break;  // ← gracefully stops the iterator
  process(item);
}
요청마다 같은 문맥을 유지하는 서버·javascript
// AsyncLocalStorage in a real-feeling server
import { createServer } from 'node:http';
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

const als = new AsyncLocalStorage();

function log(level, msg) {
  const ctx = als.getStore();
  console.log(`[${ctx?.requestId ?? '-'}] ${level} ${msg}`);
}

async function deepCall() {
  log('info', 'deep work happening');
  await new Promise(r => setTimeout(r, 50));
  log('info', 'deep work done');
}

const server = createServer((req, res) => {
  als.run({ requestId: randomUUID() }, async () => {
    log('info', `${req.method} ${req.url}`);
    await deepCall();    // sees the same requestId
    res.end('ok');
  });
});
server.listen(3000);

External links

Exercise

take(n, asyncIterable)를 만들어 비동기 순회 대상에서 최대 n개를 읽어 배열로 돌려줘. 다음으로 pollUntil(predicate, asyncIterable, signal)을 만들어 조건이 참이 되거나 취소 신호가 올 때까지 값을 읽게 해. 위의 페이지 단위 API 생성기를 테스트 입력으로 사용해.
Hint
take에서는 배열에 값을 넣고 길이가 n이 되면 반복문을 끝내면 돼. pollUntil은 값을 기다리기 전과 받은 뒤에 signal.aborted를 확인해. 입력 자체가 { signal }을 받는다면 같은 신호를 연결해야 실제 네트워크나 파일 작업까지 취소돼.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.