숏컷 코드
import { AsyncLocalStorage } from "node:async_hooks";
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
const requestContext = new AsyncLocalStorage();
function log(message) {
const store = requestContext.getStore();
console.log(store?.requestId ?? "-", message);
}
createServer((req, res) => {
requestContext.run({ requestId: randomUUID() }, async () => {
log(`${req.method} ${req.url}`);
await handleRequest(req, res);
log("done");
});
});| 목적 | 먼저 볼 API |
|---|---|
| 비동기 체인에 값 심기 | asyncLocalStorage.run(store, callback) |
| 현재 컨텍스트 읽기 | asyncLocalStorage.getStore() |
| 콜백을 현재 컨텍스트에 묶기 | AsyncLocalStorage.bind(fn) |
| 컨텍스트 손실 지점 감싸기 | AsyncResource 또는 snapshot() |
저장소 범위
AsyncLocalStorage는 특정 비동기 흐름 안에서만 보이는 저장소를 만듭니다. run() 안에서 시작된 Promise, timer, I/O 콜백은 같은 store를 이어받을 수 있고, getStore()는 현재 실행 중인 비동기 컨텍스트의 값을 돌려줍니다.
requestContext.run({ requestId: "req-1" }, async () => {
await saveLog();
console.log(requestContext.getStore().requestId);
});요청마다 다른 store를 넣으면 함수 인자를 계속 전달하지 않아도 로깅, tracing, audit 정보가 호출 체인 전체에서 읽힙니다. 다만 store는 전역 상태가 아니라 "현재 비동기 실행 흐름에 붙은 값"으로 읽어야 합니다.
function currentRequestId() {
return requestContext.getStore()?.requestId;
}언제 쓸까
HTTP 요청 ID를 로그에 자동으로 붙이는 경우가 가장 흔합니다. API handler, service, repository 계층까지 requestId 인자를 계속 넘기는 대신 공통 logger가 현재 컨텍스트에서 값을 읽도록 만들 수 있습니다.
function createLogger() {
return {
info(message) {
const requestId = requestContext.getStore()?.requestId;
console.log(JSON.stringify({ level: "info", requestId, message }));
},
};
}tenant ID, locale, 사용자 ID, trace span ID처럼 요청 단위로 유지되어야 하지만 비즈니스 함수 시그니처를 오염시키고 싶지 않은 값에도 적합합니다. 반대로 함수의 실제 입력값이나 권한 판단에 필수인 값은 명시 인자로 넘기는 편이 더 안전합니다.
손실 지점
대부분의 Promise와 Node 내장 비동기 API에서는 컨텍스트가 유지됩니다. 하지만 외부 네이티브 addon, 특수한 callback bridge, 직접 만든 event dispatch 구조에서는 컨텍스트가 끊길 수 있습니다. 이때는 AsyncLocalStorage.bind()나 AsyncResource를 검토합니다.
const boundHandler = AsyncLocalStorage.bind(() => {
log("called later");
});
thirdPartyCallbackApi(boundHandler);컨텍스트가 없을 수 있는 함수에서는 getStore() 결과를 항상 선택적으로 다룹니다. worker thread나 별도 프로세스로 넘어가는 값은 자동 전파되지 않으므로 메시지 payload에 필요한 컨텍스트를 직접 넣어야 합니다.
주의할 점
AsyncLocalStorage를 요청 단위 캐시나 큰 객체 보관소처럼 쓰면 수명과 메모리 사용을 추적하기 어려워집니다. store에는 request ID, trace ID처럼 작고 불변에 가까운 값만 두고, 실제 데이터는 명시적인 인자나 저장소 경계에서 다루십시오.
enterWith()는 현재 동기 실행 흐름에 컨텍스트를 강제로 넣고 이후 비동기 호출에 이어지게 합니다. 이벤트 핸들러가 여러 개 붙은 구조에서는 의도보다 넓게 전파될 수 있으므로 일반 요청 처리에서는 run()을 먼저 선택하는 편이 안전합니다.
참고 링크
1 sources