빠른 비교
import { spawn, execFile } from "node:child_process";
const child = spawn("git", ["log", "--oneline", "-20"]);
child.stdout.on("data", (chunk) => process.stdout.write(chunk));
child.stderr.on("data", (chunk) => process.stderr.write(chunk));
const install = spawn("npm", ["install"], { stdio: "inherit" });
install.on("close", (code) => {
if (code !== 0) process.exit(code);
});
execFile("echo", ["safe input"], (err, stdout) => {
console.log(stdout);
});프로세스 실행
먼저 머릿속에 들어와야 하는 기본형은 아래입니다.
- 스트림으로 붙이기:
spawn(cmd, args) - 결과 문자열 한 번에 받기:
exec(command) - shell 없이 안전 실행:
execFile(file, args) - 부모 터미널 그대로 연결:
spawn(cmd, args, { stdio: "inherit" }) - shell 기능이 꼭 필요할 때만:
exec(...)또는spawn(..., { shell: true })
출력이 크거나 실시간이면 spawn
빌드 로그, 장시간 실행, 대용량 stdout이면 spawn이 기본입니다. 스트림으로 다루니까 메모리 한계에 덜 걸리고 중간 진행도 볼 수 있습니다.
짧은 명령 결과만 문자열로 받으면 exec
결과를 한 번에 문자열로 받고 끝내는 짧은 명령이라면 exec가 편합니다. 다만 shell을 거친다는 점을 같이 기억해야 합니다.
또 exec는 결과를 버퍼에 모으므로, 로그가 길어질 수 있는 명령에는 기본 선택으로 두기 어렵습니다.
외부 입력이 섞이면 execFile이 더 자연스럽다
실행 파일 경로와 인자를 분리할 수 있고, 기본적으로 shell을 통하지 않아서 안전한 기본값에 가깝습니다.
execFile("git", ["status", "--short"], (err, stdout) => {
if (err) throw err;
console.log(stdout);
});외부 입력이 섞이면 execFile 또는 spawn
사용자 입력을 명령 문자열에 붙이는 순간 shell injection 위험이 커집니다. 인자를 배열로 나누는 방식이 기본값입니다.
stdio: "inherit"은 개발 도구 연결에 좋다
테스트, 빌드, lint처럼 부모 터미널에 그대로 보여주면 되는 명령은 별도 가공보다 inherit가 더 단순합니다.
shell 기능이 필요할 때만 exec나 shell: true
파이프, 글롭, 리다이렉션 같은 shell 문법이 실제로 필요할 때만 shell을 끼웁니다. 그렇지 않다면 실행 파일과 인자를 분리하는 쪽이 보안과 디버깅 모두에서 낫습니다.
종료 코드와 signal도 같이 본다
자식 프로세스 카드는 stdout만이 아니라 성공/실패 판정도 같이 다룹니다.
close나 exit 이벤트에서 code와 signal을 같이 봐야, 사용자가 중단했는지 명령 자체가 실패했는지 구분이 됩니다.
언제 spawn을 쓸까
체크포인트
- 짧은 문자열 결과:
exec() - 대용량/실시간 출력:
spawn() - shell 없이 안전 실행:
execFile() - 터미널 그대로 연결:
spawn(..., { stdio: "inherit" }) - 파이프/리다이렉션 같은 shell 문법이 꼭 필요하다:
exec()또는shell: true - Node IPC 전용:
fork() - 성공/실패 판정은 종료 code와 signal을 같이 본다
주의할 점
외부 입력이 붙은 exec()는 거의 바로 보안 문제로 이어진다. 또 결과가 길어지는 명령에 exec()를 쓰면 버퍼 한계와 메모리 부담까지 같이 따라온다. 사용자가 만든 문자열을 shell이 해석하게 두지 않는 쪽이 기본이다.
참고 링크
1 sources