기존 javascript/ 문서들이 다루지 않는 Node 백엔드 실전 패턴 두 가지를 묶어 정리한다. - HTTP 클라이언트 / fetch / Ky / undici 비교 → http-client.md - AbortController / fetch 취소 / timeout 구현 → abort-controller.md - V8·이벤트 루프·메모리·CPU 운영 가...
기존 javascript/ 문서들이 다루지 않는 Node 백엔드 실전 패턴 두 가지를 묶어 정리한다.
readFileSync 대신 Streamsfs.readFileSyncfs.readFileSync는 파일 전체를 한 번에 읽어 Node 프로세스 힙에 Buffer / String 으로 올린다. 파일이 커지면 메모리 사용량과 GC 부담이 비례해서 늘고, 처리 완료 전까지 메모리 해제가 불가능하다. CSV 한 건이 수백 MB 단위로 커질 가능성이 있는 운영 환경이라면 그대로 OOM 으로 이어진다.
fs.createReadStream파일을 작은 chunk 단위로 읽고, 읽자마자 처리하고, 처리가 끝나면 버린다. 메모리에는 chunkSize 수준만 올라가 GC 부담이 적고, CSV 처럼 줄 단위로 처리 가능한 포맷에 잘 맞는다.
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const rl = createInterface({
input: createReadStream('big.csv', { encoding: 'utf8' }),
crlfDelay: Infinity,
});
for await (const line of rl) {
await processLine(line);
}생산자(파일 읽기 등)가 소비자(DB Insert, 외부 API 호출 등)보다 빠를 때 데이터가 메모리에 누적되어 OOM 으로 이어지는 상황을 막기 위해, 소비자의 처리 속도에 맞춰 생산자의 속도를 늦추는 메커니즘.
highWaterMark 옵션, 기본값 64KB).for (const chunk of readable) 처럼 동기 루프로 받으면서 안에서 await db.insert(chunk) 만 호출 — 이러면 백프레셔가 흐르지 않고 메모리에 누적될 수 있다. pipeline() 으로 잇거나 async iterator (for await) 를 쓰는 게 맞다.data 이벤트로 직접 받은 chunk 를 비동기로 흘려보내면서 pause() / resume() 을 직접 안 부르는 경우 — 라이브러리 추상화에 맡기는 게 안전하다.pipe() vs pipeline()pipe()가장 단순한 연결. 한쪽 스트림의 출력을 다른쪽 입력으로 흘려보낸다. 작동은 하지만:
pipeline() (Node 10+)여러 스트림을 한 번에 잇고, 에러가 나면 모든 스트림을 정리하고 콜백 / Promise 로 에러를 전달한다. 운영 환경에서는 사실상 pipeline() 만 써야 한다.
import { pipeline } from 'node:stream/promises';
await pipeline(
createReadStream('big.csv'),
csvParser,
validator,
dbWriter,
);단순 동작 확인은
pipe()로 충분하지만, 운영 환경에서 안정성을 보장해야 하는 배치는pipeline()이 의도를 더 잘 드러내고 실패를 안전하게 다룰 수 있다.
POS / 결제처럼 네트워크 지연이나 timeout 이 흔한 환경에서는 두 메커니즘을 혼동하지 말고 역할을 분리해야 한다.
멱등성 키는 시간을 가로질러 동일 요청을 연결하고, 분산 락은 순간에 동시 요청을 직렬화한다. 두 축이 다르므로 보통 같이 쓴다.