Quick Flow
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
const rl = readline.createInterface({ input, output });
const controller = new AbortController();
rl.once("SIGINT", () => controller.abort());
try {
const name = await rl.question("이름을 입력하세요: ", {
signal: controller.signal,
});
console.log(`입력: ${name}`);
} catch (error) {
if (controller.signal.aborted) process.exitCode = 130;
else throw error;
} finally {
rl.close();
}터미널 대화에는 node:readline/promises와 await rl.question()을 쓰고, Ctrl+C나 시간 제한은 question에 AbortSignal로 전달합니다. cat file | tool처럼 파이프로 들어오는 입력은 질문을 띄우지 말고 for await...of로 줄을 읽습니다. 어떤 흐름이든 interface는 finally에서 닫습니다.
입력 흐름
먼저 머릿속에 들어와야 하는 기본형은 아래입니다.
- 대화형 프롬프트:
readline/promises - 순차 질문:
await rl.question(...) - 파이프 입력 처리:
for await (const line of rl) - 종료 보장:
try/finally+rl.close() - TTY 여부에 따라 대화형/비대화형 분기
사용자와 대화하면 question()
설정 마법사, 확인 프롬프트, 간단한 입력 수집은 Promise 기반 question()이 가장 읽기 쉽습니다.
stdin 파이프 처리면 for await...of
cat data.txt | node tool.js 같은 흐름은 인터랙션이 아니라 스트림 처리입니다. 이때는 createInterface({ input: process.stdin })와 비동기 순회가 더 맞습니다.
createInterface()는 생성 즉시 input을 소비하기 시작합니다. 인터페이스를 만든 뒤 비동기 준비 작업을 길게 한 다음 순회를 시작하면 앞선 줄을 놓칠 수 있으므로, 파이프 입력은 만들자마자 순회합니다.
종료 조건은 rl.close()
대화형 모드에서 작업이 끝났는데 프로세스가 살아 있으면 보통 이 부분입니다. try/finally로 닫는 습관이 중요합니다.
대화형인지 파이프 입력인지 먼저 나눈다
같은 CLI라도 터미널에서 직접 실행할 때와 cat file | tool처럼 파이프로 들어올 때 기대 UX가 다릅니다.
프롬프트를 띄워야 하는지, stdin을 그냥 소모해야 하는지 process.stdin.isTTY 기준으로 먼저 나누는 편이 좋습니다.
언제 readline을 쓸까
체크포인트
- 대화형 프롬프트:
node:readline/promises - 순차 질문:
await rl.question(...) - 파이프 입력 처리:
for await (const line of rl) - 종료 보장:
try/finally+rl.close() - TTY 여부에 따라 대화형/비대화형 흐름을 나눈다
주의할 점
대화형 CLI에서 rl.close()를 빼먹으면 작업은 끝났는데 프로세스가 안 꺼진다.
참고 링크
1 sources