Child Processes, Spawn, Exec, ExecFile, Fork, IPC에 대해 자세히 알아보기
원문: https://medium.com/@manikmudholkar831995/child-processes-multitasking-in-nodejs-751f9f7a85c8
이 글은 시니어 엔지니어를 위한 고급 Node.js 시리즈의 다섯 번째 글입니다. 이 글에서는 자식 프로세스가 무엇인지, 왜 필요한지, 그리고 자식 프로세스를 사용하여 최상의 성능을 얻는 방법에 대해 자세히 설명하겠습니다. 공식 문서는 child_process에 있습니다. 시니어 엔지니어를 위한 고급 Node.js 시리즈의 다른 글은 아래에서 확인할 수 있습니다.
시리즈 로드맵
목차
spawn
설정하기Node.js 애플리케이션을 실행하면 VS Code, VLC 플레이어 등의 다른 애플리케이션을 실행하는 것과 마찬가지로 자체 프로세스를 갖게 됩니다. 이 프로세스의 속성은 Node.js 앱 코드에서 접근할 수 있는 전역 객체의 process
변수에서 사용할 수 있습니다.
Node.js는 본질적으로 싱글 스레드이지만 경우에 따라 멀티 프로세스가 필요한 경우가 있습니다. 특히 동기적이고 CPU 집약적인 작업을 격리된 상태로 실행할 때 그렇습니다. 이럴 때 자식 프로세스가 필요합니다. node:child_process
모듈을 사용하면 하위 프로세스를 생성하고 메인 프로세스와 자식 프로세스 간에 프로세스 간 통신(IPC)으로 알려진 통신 채널을 설정할 수 있습니다.
이 모듈은 시간이 오래 걸리는 작업을 처리하는 것 외에도 운영 체제와 상호 작용하고 셸 명령을 실행할 수 있습니다. 간단히 말해서 자바스크립트뿐만 아니라 Git, 파이썬, PHP 또는 기타 다른 언어와 같이 다른 프로그래밍 언어도 실행할 수 있습니다.
CPU 집약적인 작업을 처리하기 위한 워커 스레드가 이미 있는데 왜 자식 프로세스가 필요한지 궁금할 수 있습니다. 결국 워커 스레드에는 자체 힙, V8 인스턴스 및 이벤트 루프가 있습니다. 그러나 동일한 프로세스 내의 워커 스레드보다 별도의 자식 프로세스를 사용하는 것이 더 유리한 경우도 있습니다. 그 이유를 설명하겠습니다.
자식 프로세스를 사용하면 외부 프로그램 또는 스크립트를 별도의 프로세스로 실행할 수 있습니다. 이는 다른 실행 파일과 상호 작용해야 할 때 특히 유용합니다.
워커 스레드와 달리 자식 프로세스는 전체 Node.js 런타임의 별도 인스턴스를 제공합니다. 각 자식 프로세스에는 자체 메모리 공간이 있으며 IPC(프로세스 간 통신)를 통해 메인 프로세스와 통신합니다. 이러한 수준의 격리는 리소스 충돌이 있거나 분리해야 하는 종속성이 있는 작업에 유용합니다.
자식 프로세스는 작업을 여러 프로세스에 분산시켜 멀티 코어 시스템의 성능을 활용할 수 있게 해줍니다. 이를 통해 더 많은 동시 요청을 처리하고 애플리케이션의 전반적인 확장성을 개선할 수 있습니다.
어떤 이유로든 자식 프로세스에 충돌이 발생해도 메인 프로세스가 함께 중단되지 않습니다. 이렇게 하면 장애가 발생하더라도 애플리케이션의 안정성과 복원력이 유지됩니다.
따라서 워커 스레드는 특정 시나리오에 적합하지만 자식 프로세스는 외부 프로그램 실행, 격리 제공, 확장성 향상 및 견고성 보장 측면에서 뚜렷한 이점을 제공합니다.
child_process
모듈을 사용하면 자식 프로세스 내부에서 시스템 명령을 실행하여 운영 체제 기능에 접근할 수 있습니다. 이러한 자식 프로세스는 동기식 및 비동기식으로 모두 생성할 수 있습니다.
const { spawn, fork, exec, execFile } = require(‘child_process’);
child_process.spawn()
, child_process.fork()
, child_process.exec()
및 child_process.execFile()
은 하위 프로세스의 생성을 비동기적으로 지원하는 메서드입니다.
각 메서드는 ChildProcess
인스턴스를 반환합니다. 이러한 객체는 Node.js EventEmitter
API를 구현하여 부모 프로세스가 자식 프로세스의 수명 주기 동안 특정 이벤트가 발생할 때 호출되는 리스너 함수를 등록할 수 있도록 합니다. 예를 들면 다음과 같습니다.
'disconnect'
이벤트는 부모 프로세스에서 subprocess.disconnect()
메서드를 호출하거나 자식 프로세스에서 process.disconnect()
를 호출한 후에 발생합니다.
프로세스를 스폰 또는 종료할 수 없거나, 자식 프로세스로 메시지를 보내거나 종료하는 데 실패하는 경우 error
이벤트가 발생합니다.
close
이벤트는 자식 프로세스의 stdio
스트림이 닫힐 때 발생합니다. 이는 여러 프로세스가 동일한 stdio
스트림을 공유할 수 있으므로 'exit'
이벤트와는 다릅니다. 'close'
이벤트는 항상 'exit'
가 이미 발생했거나 자식이 스폰되지 못한 경우, 'error
'가 발생한 후에 발생합니다.
자식 프로세스가 종료된 후 'exit'
이벤트가 발생합니다.
message
이벤트는 가장 중요한 이벤트입니다. 자식 프로세스가 process.send()
함수를 사용하여 메시지를 보낼 때 발생합니다. 이것이 부모/자식 프로세스가 서로 통신할 수 있는 방법입니다.
'spawn'
이벤트는 자식 프로세스가 성공적으로 스폰되면 발생합니다. 자식 프로세스가 성공적으로 스폰되지 않으면 'spawn'
이벤트가 발생하지 않고 'error'
이벤트가 대신 발생합니다.
.spawn()
메서드는 실행할 명령, 해당 명령에 전달할 문자열 배열 형식의 인자, 그리고 프로세스가 생성되는 설정을 재정의할 수 있는 옵션 객체를 전달하는 자식 프로세스를 만드는 데 사용할 수 있습니다. 예를 들어, 옵션에는 환경 변수인 env
, 셸 내부에서 명령을 실행하는 shell
, 부모가 종료된 후에도 자식 프로세스가 계속 실행될지 여부인 detached
, 자식 프로세스를 중단하는 데 사용할 수 있는 signal
등이 있습니다. 이러한 옵션은 spawn의 공식 문서에서 확인할 수 있습니다.
.spawn()
메서드가 다른 프로세스 생성 메서드와 차별화되는 점은 새로운 프로세스에서 외부 애플리케이션을 스폰하고 I/O에 대한 스트리밍 인터페이스를 반환한다는 것입니다. 이로 인해 대량의 데이터를 생성하는 애플리케이션을 처리하거나 데이터를 읽어 들이면서 작업하는 데 적합합니다. 스트림 기반 I/O는 다음과 같은 이점을 제공할 수 있습니다.
적은 메모리 사용량
자동 백프레셔 처리
버퍼링된 청크에서 데이터를 지연하여 생성하거나 소비합니다.
이벤트 기반 및 논 블로킹
버퍼를 사용하면 V8 힙 메모리 한계를 극복할 수 있습니다.
모든 자식 프로세스에는 child.stderr
, child.stdout
(읽기 가능한 스트림) 및 child.stdin
(쓰기 가능한 스트림)을 사용하여 접근할 수 있는 세 개의 표준 stdio
스트림도 있습니다. 이러한 스트림은 이벤트 이미터이며 모든 자식 프로세스에 연결된 해당 stdio
스트림에서 다양한 이벤트를 수신할 수 있습니다. child.stdout
및 child.stderr
의 경우, 명령의 출력 또는 명령을 실행하는 동안 발생한 오류를 포함하는 data
이벤트를 수신할 수 있습니다.
ls -lh /usr
을 실행하여 stdout
, stderr
및 종료 코드를 캡처하는 예제 Linux/Unix 시스템에서 이 예제를 따라해 보세요.
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
출력
복잡한 예제를 통해 한 단계 더 발전시켜 보겠습니다. 여기서는 ps | grep bash
를 실행하려고 합니다. ps
명령은 진행 중인 프로세스를 반환하고 grep
은 일치하는 패턴을 검색하는 데 유용한 명령으로 여기서는 'bash'를 검색하려고 합니다. ps
에 대해 출력 스트림(예시: ps.stdout
)을 grep
의 입력 스트림(예시: grep.stdin.write
)에 쓰려고 하는 하나의 프로세스가 스폰됩니다. ps
가 완료되면 grep
의 입력 스트림이 종료되는 시점에 close
를 호출하고 grep
명령이 실행됩니다. 아래는 index.js 내부에 작성된 내용입니다.
const { spawn } = require('node:child_process')
const ps = spawn('ps')
const grep = spawn('grep', ['bash'])
ps.stdout.on('data', (data) => {
grep.stdin.write(data)
})
ps.stderr.on('data', (data) => {
console.error(`ps stderr: ${data}`)
})
ps.on('close', (code) => {
if (code !== 0) {
console.log(`ps process exited with code ${code}`)
}
grep.stdin.end()
})
grep.stdout.on('data', (data) => {
console.log(data.toString())
})
grep.stderr.on('data', (data) => {
console.error(`grep stderr: ${data}`)
})
grep.on('close', (code) => {
if (code !== 0) {
console.log(`grep process exited with code ${code}`)
}
})
출력
윈도우에서 실행하는 경우, .bat
및 .cmd
파일은 shell
옵션이 설정된 child_process.spawn()
, child_process.exec()
을 사용하거나 cmd.exe
를 스폰하고 .bat
또는 .cmd
파일을 인자로 전달하여 호출할 수 있습니다(이는 shell
옵션 및 child_process.exec()
이 수행하는 작업입니다). 어떤 경우든 스크립트 파일 이름에 공백이 포함되어 있으면 따옴표로 묶어야 합니다.
.fork()
는 새 프로세스에서 Node.js 스크립트를 실행하고 두 프로세스 간에 IPC 통신 채널을 원하는 경우에 특히 유용합니다. child_process.fork()
메서드는 새로운 Node.js 프로세스를 스폰하는 데 사용되는 child_process.spawn()
의 특수한 경우입니다. child_process.spawn()
과 마찬가지로 ChildProcess
객체가 반환됩니다. 반환된 ChildProcess
에는 부모와 자식 간에 메시지를 주고받을 수 있는 추가 통신 채널이 내장되어 있습니다.
fork 메서드는 Node.js 프로세스 간에 메시지를 전달할 수 있는 IPC 채널을 엽니다.
자식 프로세스에서는 process.on('message') 및 process.send('message to parent')를 사용하여 데이터를 수신하고 전송할 수 있습니다.
부모 프로세스에서는 child.on('message') 및 child.send('message to child')가 사용됩니다.
index.js
의 간단히 예제를 살펴보겠습니다.
const { fork } = require('child_process');
const forked = fork('child_program.js');
forked.on('message', (msg) => {
console.log('Message from child', msg);
});
forked.send('hello world');
child_program.js
은 다음과 같습니다.
process.on('message', (msg) => {
console.log('Message from parent:', msg);
});
let counter = 0;
setInterval(() => {
process.send({ counter: counter++ });
}, 1000);
출력
부모에서 자식으로 메시지를 전달하려면 포크된 객체 자체에서 send
함수를 실행한 다음에 자식 스크립트에서 전역 process
객체의 message
이벤트를 수신할 수 있습니다.
위의 parent.js
파일을 실행하면 먼저 포크된 자식 프로세스에서 출력할 'hello world'
를 전송하고 포크된 자식 프로세스는 매초마다 증가된 카운터 값을 전송하여 부모 프로세스에서 출력되도록 합니다.
좀 더 실용적인 예제를 살펴보겠습니다. 아래 예제에서는 각각 "normal" 또는 "special" 우선 순위로 연결을 처리하는 두 개의 자식을 스폰합니다.
index.js
는 다음과 같습니다.
const { fork } = require('node:child_process');
const normal = fork('child_program.js', ['normal']);
const special = fork('schild_program.js', ['special']);
// 서버를 열고 소켓을 자식에게 보냅니다.
// pauseOnConnect 옵션을 사용하여 소켓이 자식 프로세스로 전송되기 전에 읽히지 않도록 합니다.
const server = require('node:net').createServer({ pauseOnConnect: true });
server.on('connection', (socket) => {
// special 우선 순위인 경우
if (socket.remoteAddress === '74.125.127.100') {
special.send('socket', socket);
return;
}
// normal 우선 순위인 경우
normal.send('socket', socket);
});
server.listen(1337);
child_program.js
는 다음과 같습니다.
process.on('message', (m, socket) => {
if (m === 'socket') {
if (socket) {
// 클라이언트 소켓이 있는지 확인합니다.
// 소켓이 전송된 시간과 자식 프로세스에서 수신된 시간 사이에 닫힐 수 있습니다
socket.end(`Request handled with ${process.argv[2]} priority`);
}
}
});
위의 예제는 remoteAddress
에 따라 소켓이 해당 자식 프로세스에 전달됩니다. 즉, 특별한 remoteAddress
인 경우, special
하위 프로세스에 전달되고 그렇지 않으면 normal
하위 프로세스에 전달됩니다. 하위 프로세스로 전달된 소켓에서 .maxConnections
를 사용하지 마세요. 부모는 소켓이 파괴된 시기를 추적할 수 없습니다. 하위 프로세스의 모든 'message'
핸들러는 연결을 자식에게 보내는 데 걸리는 시간 동안 연결이 닫혔을 수 있으므로 socket
이 존재하는지 확인해야 합니다.
셸 구문을 사용해야 하고 명령에서 예상되는 데이터 크기가 작은 경우 exec
함수가 좋은 선택입니다. 명령에서 생성된 출력을 버퍼링하고 전체 출력 값을 콜백 함수에 전달합니다(반면, spawn
은 스트림을 사용하여 데이터를 처리합니다).
exec
은 셸을 스폰한 다음 해당 셸 내에서 command
를 실행합니다. callback
함수가 제공되면 인자 (error, stdout, stderr)
와 함께 호출됩니다. 성공하면 error
는 null
이 됩니다. 오류가 발생하면 error
는 Error
의 인스턴스가 됩니다. error.code
속성은 프로세스의 종료 코드가 됩니다. 콜백에 전달된 stdout
및 stderr
인자는 자식 프로세스의 stdout
및 stderr
출력을 포함합니다.
cat
명령으로 index.js
를 읽고 wc -l
로 결과 줄 즉, 코드 줄을 세는 간단한 예제를 살펴보겠습니다.
const { exec } = require('node:child_process')
exec('cat index.js | wc -l', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`)
return
}
console.log(`stdout: ${stdout}`)
console.error(`stderr: ${stderr}`)
})
출력
흥미로운 반전은 Options 객체에서 몇 가지 설정을 제공하여 exec에 추가할 수 있다는 것입니다. 예를 들어 cwd
옵션을 사용하여 스크립트의 작업 디렉터리를 변경할 수 있습니다. 예를 들어 위의 예제는 다음과 같이 다른 디렉터리에서 실행하도록 만들 수 있습니다.
exec
함수는 셸을 사용하여 명령을 실행하므로 여기에서 셸 구문 을 사용하여 직접 셸 파이프 기능을 활용할 수 있습니다
셸을 사용하지 않고 파일을 실행해야 하는 경우, execFile
함수가 필요합니다. exec
함수와 동일하게 작동하지만 셸을 사용하지 않기 때문에 좀 더 효율적입니다.
const { execFile } = require('node:child_process');
const child = execFile('node', ['--version'], (error, stdout, stderr) => {
if (error) {
throw error;
}
console.log(stdout);
});
출력
Windows에서 .bat
또는 .cmd
파일과 같은 일부 파일은 자체적으로 실행할 수 없습니다. 이러한 파일은 execFile
로 실행할 수 없으며 실행하려면 exec
또는 셸이 true로 설정된 상태에서 spawn
이 필요합니다.
.spawnSync
, .execSync
및 .execFileSync
메서드는 동기식이며 Node.js 이벤트 루프를 블로킹하여 스폰된 프로세스가 종료될 때까지 추가 코드의 실행을 일시 중지합니다.
이러한 블로킹 호출은 일반적으로 스크립팅 작업을 단순화하고 시작 시 애플리케이션 구성의 로딩/처리를 단순화하는 데 유용합니다.
자식 프로세스를 종료하는 방법은 몇 가지가 있습니다.
ChildProcess 객체에서 .kill()
을 사용합니다.
옵션 객체의 timeout
옵션을 사용하고 프로세스가 실행될 수 있는 최대 시간을 밀리초 단위로 설정해야 합니다. 기본값: undefined
signal
을 사용하고 signal
옵션이 활성화된 경우, 해당 AbortController
에서 .abort()
를 호출하는 것은 콜백에 전달되는 오류를 제외하면 자식 프로세스에서 .kill()
을 호출하는 것과 유사합니다.
const { spawn } = require('node:child_process');
const controller = new AbortController();
const { signal } = controller;
const grep = spawn('grep', ['ssh'], { signal });
grep.on('error', (err) => {
// 컨트롤러가 중단되면 err가 AbortError인 상태로 호출됩니다.
});
controller.abort(); // 자식 프로세스를 중지합니다.
stdio
옵션은 자식 프로세스의 입출력 대상을 결정하는 역할을 합니다. 배열 또는 문자열로 할당할 수 있습니다. 문자열 값은 일반적으로 사용되는 배열 구성으로 자동 변환되는 편리한 단축키 역할을 합니다.
기본적으로 stdio는 다음과 같이 구성됩니다.
stdio: 'pipe'
위 값은 다음의 배열 값에 대한 약어입니다.
stdio: [ 'pipe', 'pipe', 'pipe' ]
즉, ChildProcess 객체는 파일 디스크립터 0-2에 접근할 수 있는 스트림(child.stdio, child.stdio[1], child.stdio[2]
)를 갖고 있다는 것을 의미합니다.
I/O를 다른 곳으로 전달하고 싶다면 파일 디스크립터를 지정할 수 있는 옵션이 있습니다. 반면, 완전히 무시하고 싶다면 'ignore
'를 사용할 수 있습니다.
예를 들어, 자식 프로세스에 입력을 제공하지 않을 것이므로 FD 0 (stdin
)을 무시하고 출력 FD 1 (stdout
)과 오류 FD 2 (stderr
)를 별도의 로그 파일에 캡처하고 싶다고 가정해 보겠습니다. 이는 다음과 같이 설정할 수 있습니다.
let fs = require('fs')
let cp = require('child_process')
let outFd = fs.openSync('./outputlogs', 'a')
let errFd = fs.openSync('./errorslogs', 'a')
let child = cp.spawn('ls', [], {
stdio: ['ignore', outFd, errFd]
})
실행 후 출력 로그
이것은 Unix 철학입니다. 한 가지 일을 잘하는 프로그램을 작성하세요. 함께 작동하는 프로그램을 작성하세요. 텍스트 스트림을 처리하는 프로그램을 작성하는 것은 보편적인 인터페이스이기 때문입니다.
하나의 프로세스의 출력을 다음 프로세스에 전달하고, 그 다음 프로세스로 이어지도록 프로그램을 작성해 보겠습니다. cat
명령은 파일에서 데이터를 읽고, 이 데이터는 sort
명령의 입력으로 전달되어 정렬된 줄을 출력으로 제공합니다. 그리고 이 출력은 다시 uniq
명령의 입력으로 전달되어 중복된 줄을 제거합니다.
filesToBeChecked.txt
은 다음과 같습니다.
LOL
LMAO
ROLF
LOL
GTG
index.js
는 다음과 같습니다.
let cp = require('child_process')
let cat = cp.spawn('cat', ['filesToBeChecked.txt'])
let sort = cp.spawn('sort')
let uniq = cp.spawn('uniq')
cat.stdout.pipe(sort.stdin)
sort.stdout.pipe(uniq.stdin)
uniq.stdout.pipe(process.stdout)
여기서 각 명령의 출력은 다음 명령의 입력이 됩니다.
자식 프로세스가 셸에 접근할 수 있도록 허용할 때는 주의해야 합니다. 특히 외부 소스의 동적 입력을 처리할 때 셸 구문을 사용하면 보안 위험을 초래할 수 있습니다. 이는 사용자가 ';
' 및 '$
'와 같은 셸 구문 문자를 악용하여 악성 명령을 실행할 수 있는 잠재적인 명령 주입 공격의 여지를 남깁니다. 예를 들어, command + '; rm -rf ~'
와 같은 명령을 입력하여 중요한 파일을 삭제할 수 있습니다.
예시를 들어 보겠습니다 (이 작업은 시스템에서 수행하지 마세요).
사용자가 입력한 명령을 받아서 exec
를 통해 해당 명령을 실행하는 프로세스가 있다고 가정해 보겠습니다. 해당 코드는 다음과 같습니다.
cp.exec('something hardcoded command' + req.query.userInput);
악의적인 사용자가 “; rm -rf / ;
”를 입력한다고 가정해 보겠습니다.
아직 이해하지 못했다면, 이 메시지는 "새로운 명령을 시작하고 (;), 파일 시스템의 핵심에 있는 모든 파일과 디렉토리를 강제로 완전히 삭제한 다음 (rm -rf /), 그 뒤에 오는 것이 있다면 명령을 종료하라 (;)"는 의미입니다.
셸 기능 없이 애플리케이션을 실행하려는 경우, execFile
을 대신 사용하는 것이 실제로 더 안전하고 조금 더 빠릅니다.
cp.execFile('something hardcoded command', [req.query.schema]);
이 경우, 악성 주입 공격은 셸에서 실행되지 않고 외부 애플리케이션이 인자를 이해하지 못하여 오류를 발생시키기 때문에 실패하게 됩니다.
몇 가지 유의해야 할 사항은 다음과 같습니다.
기본적으로 부모 프로세스는 분리된 자식 프로세스가 종료될 때까지 기다립니다.
부모 프로세스와 노드 간의 연결을 유지하는 몇 가지 요소가 있는데, 이는 부모 프로세스에서 자식 프로세스에 대한 ref
와 부모와 자식 간에 형성된 통신 채널입니다.
자식 프로세스를 독립적으로 실행하려면 다음과 같은 몇 가지 작업이 필요합니다.
자식 프로세스가 종료된 후에도 부모 프로세스가 계속 실행되도록 하려면, 옵션 객체의 설정 중 하나인 options.detached
설정을 사용하면 됩니다.
Windows에서는 options.detached
를 true
로 설정하면 부모 프로세스가 종료된 후에도 자식 프로세스가 계속 실행될 수 있습니다. 한 번 활성화하면 다시 비활성화할 수 없습니다.
Windows 이외의 플랫폼에서 options.detached
를 true
로 설정하면 자식 프로세스는 새로운 프로세스 그룹과 세션의 리더가 됩니다. 자식 프로세스는 분리 여부와 관계없이 부모가 종료된 후에도 계속 실행될 수 있습니다.
부모의 이벤트 루프에서 자식 프로세스를 참조하면 부모가 종료되지 않습니다. 이 참조를 제거하려면 해당 자식 프로세스에서 .unref()
를 호출하면 됩니다. (유사하게 .ref()
를 호출하여 참조를 다시 추가할 수 있습니다.)
options.stdio
는 부모와 자식 간의 채널을 나타냅니다. options.stdio
옵션은 부모와 자식 프로세스 간에 설정되는 파이프를 구성하는 데 사용됩니다. 이 옵션을 'ignore
'로 설정하면 이 통신 채널을 무시하도록 지시합니다. 자세한 내용은 공식 문서에서 확인하세요.
부모 프로세스의 종료를 무시하기 위해 부모 stdio 파일 디스크립터를 분리하고 무시하는 장기 실행 프로세스의 예제는 다음과 같습니다.
const { spawn } = require('node:child_process');
const subprocess = spawn(process.argv[0], ['child_program.js'], {
detached: true,
stdio: 'ignore',
});
subprocess.unref();
좀 더 복잡한 예를 들어 보겠습니다. options.stdio
를 사용하면 스트림을 정의할 수 있습니다. 예를 들어, 입력 스트림으로 파이프를, 출력 스트림으로 파일 디스크립터를, 오류 스트림으로 현재 메인 프로세스의 오류 스트림을 전달하고 싶다면, 이 옵션은 ['pipe', fd, process.stderr]
과 같이 보일 것입니다.
모든 std 스트림을 무시하고 싶다면 이전 예제에서 했던 것처럼 'ignore
'를 전달하면 됩니다. 'ignore
'를 전달하는 것은 ['ignore', 'ignore', 'ignore']
를 전달하는 것과 동일합니다. ignore 외에도 pipe, inherit, overlapped, ipc, null, undefined
와 같은 다른 옵션이 있습니다. 공식 문서에서 자세한 내용을 확인하세요.
자식 프로세스에 파일 디스크립터를 출력 스트림으로 전달하여 자식이 주어진 파일에 출력을 쓸 수 있도록 하는 예제를 보여드리겠습니다.
index.js
는 다음과 같습니다.
const fs = require('node:fs')
const { spawn } = require('node:child_process')
const out = fs.openSync('./out.log', 'a')
const subprocess = spawn('node', ['child_program.js'], {
detached: true,
stdio: ['ignore', out, process.stderr]
})
subprocess.unref()
child_program.js
는 다음과 같습니다.
const { spawn } = require('node:child_process')
const ls = spawn('ls', ['-lh', '/usr'])
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`)
})
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`)
})
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`)
})
출력
위의 예제는 fork
로 작성해도 동일한 결과를 얻을 수 있습니다.
const fs = require('node:fs')
const { fork } = require('node:child_process')
const out = fs.openSync('./out.log', 'a')
const subprocess = fork('child_program.js', [], {
detached: true,
stdio: ['ipc', out, process.stderr]
})
subprocess.unref()
spawn
설정하기자식 프로세스가 부모의 표준 IO 객체를 상속받도록 만들 수 있지만, 더 중요한 것은 spawn
함수가 셸 구문도 사용하도록 설정할 수 있다는 것입니다.
아래 예제를 살펴보겠습니다.
child_program.js
는 다음과 같습니다.
const { spawn } = require('node:child_process')
const ls = spawn('ls', ['-lh', '/usr'])
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`)
})
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`)
})
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`)
})
stdio: 'inherit' & shell: true
를 사용하지 않는 예제입니다.
index.js
는 다음과 같습니다.
const { spawn } = require('node:child_process')
const ps = spawn('node child_program.js', {
})
출력
spawn이 셸 구문을 이해하지 못해 오류가 발생했습니다.
셸 옵션을 추가해 보겠습니다.
index.js
는 다음과 같습니다.
const { spawn } = require('node:child_process')
const ps = spawn('node child_program.js', {
shell: true
})
출력
이제 몇 가지 말할 수 있는 것은 spawn이 셸 구문을 이해하고 child_program을 실행할 수 있지만, 출력이 보이지 않는 이유는 현재 보고 있는 터미널/콘솔이 자식 프로세스가 아닌 메인 프로세스의 표준 IO 스트림에 연결되어 있기 때문입니다. 따라서 자식 프로세스가 결과를 메인 프로세스의 터미널에 출력하도록 하려면, 메인 IO 스트림을 자식 프로세스와 공유해야 합니다. 이를 위해 stdio: 'inherit'
옵션을 사용할 수 있습니다.
stdio: 'inherit'
옵션을 추가해 보겠습니다.
const { spawn } = require('node:child_process')
const ps = spawn('node child_program.js', {
stdio: 'inherit',
shell: true
})
출력
stdio: 'inherit'
옵션 덕분에 코드를 실행할 때 자식 프로세스가 부모 프로세스의 stdin
, stdout
, 및 stderr
를 상속받습니다. 이로 인해 자식 프로세스의 데이터 이벤트 핸들러가 메인 process.stdout
스트림에서 트리거되어 스크립트가 결과를 바로 출력하게 됩니다.
위의 shell: true
옵션 덕분에 exec
와 마찬가지로 전달된 명령에서 셸 구문을 사용할 수 있었습니다. 하지만 이 코드를 사용하면 spawn
함수가 제공하는 데이터 스트리밍의 이점을 여전히 누릴 수 있습니다.