[번역] 자식 프로세스: Node.js의 멀티태스킹

Sonny·2일 전
3

Article

목록 보기
31/31
post-thumbnail

Child Processes, Spawn, Exec, ExecFile, Fork, IPC에 대해 자세히 알아보기

원문: https://medium.com/@manikmudholkar831995/child-processes-multitasking-in-nodejs-751f9f7a85c8

이 글은 시니어 엔지니어를 위한 고급 Node.js 시리즈의 다섯 번째 글입니다. 이 글에서는 자식 프로세스가 무엇인지, 왜 필요한지, 그리고 자식 프로세스를 사용하여 최상의 성능을 얻는 방법에 대해 자세히 설명하겠습니다. 공식 문서는 child_process에 있습니다. 시니어 엔지니어를 위한 고급 Node.js 시리즈의 다른 글은 아래에서 확인할 수 있습니다.

시리즈 로드맵

목차

  • 자식 프로세스는 정확히 무엇인가요?
  • 왜 필요한걸까요?
    • 외부 프로그램 실행
    • 개선된 격리성
    • 개선된 확장성
    • 개선된 견고성
  • 자식 프로세스 생성
    • .spawn()을 사용하여 프로세스 생성
    • .fork()를 사용하여 프로세스 생성
    • .exec()를 사용하여 프로세스 생성
    • execFile()을 사용하여 프로세스 생성
    • 동기 프로세스 생성
    • 언제 무엇을 사용해야 하나요?
  • 자식 프로세스 Abort/Stop/Kill
  • 자식 프로세스와 부모 프로세스 간의 I/O 처리
    • 스트림 연결
  • 명령 실행 시 보안
  • 자식 프로세스가 부모 프로세스와 독립적으로 실행되도록 하기
  • 셸 구문을 사용하고 부모의 표준 IO를 상속받도록 spawn 설정하기

자식 프로세스는 정확히 무엇인가요?

alteration.gif

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()을 사용하여 프로세스 생성

.spawn() 메서드는 실행할 명령, 해당 명령에 전달할 문자열 배열 형식의 인자, 그리고 프로세스가 생성되는 설정을 재정의할 수 있는 옵션 객체를 전달하는 자식 프로세스를 만드는 데 사용할 수 있습니다. 예를 들어, 옵션에는 환경 변수인 env, 셸 내부에서 명령을 실행하는 shell, 부모가 종료된 후에도 자식 프로세스가 계속 실행될지 여부인 detached, 자식 프로세스를 중단하는 데 사용할 수 있는 signal 등이 있습니다. 이러한 옵션은 spawn의 공식 문서에서 확인할 수 있습니다.

.spawn() 메서드가 다른 프로세스 생성 메서드와 차별화되는 점은 새로운 프로세스에서 외부 애플리케이션을 스폰하고 I/O에 대한 스트리밍 인터페이스를 반환한다는 것입니다. 이로 인해 대량의 데이터를 생성하는 애플리케이션을 처리하거나 데이터를 읽어 들이면서 작업하는 데 적합합니다. 스트림 기반 I/O는 다음과 같은 이점을 제공할 수 있습니다.

  • 적은 메모리 사용량

  • 자동 백프레셔 처리

  • 버퍼링된 청크에서 데이터를 지연하여 생성하거나 소비합니다.

  • 이벤트 기반 및 논 블로킹

  • 버퍼를 사용하면 V8 힙 메모리 한계를 극복할 수 있습니다.

모든 자식 프로세스에는 child.stderr, child.stdout(읽기 가능한 스트림) 및 child.stdin(쓰기 가능한 스트림)을 사용하여 접근할 수 있는 세 개의 표준 stdio 스트림도 있습니다. 이러한 스트림은 이벤트 이미터이며 모든 자식 프로세스에 연결된 해당 stdio 스트림에서 다양한 이벤트를 수신할 수 있습니다. child.stdoutchild.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}`);
});

output1.png

출력

복잡한 예제를 통해 한 단계 더 발전시켜 보겠습니다. 여기서는 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}`)
 }
})

output2.png

출력

윈도우에서 실행하는 경우, .bat.cmd 파일은 shell 옵션이 설정된 child_process.spawn(), child_process.exec()을 사용하거나 cmd.exe를 스폰하고 .bat 또는 .cmd 파일을 인자로 전달하여 호출할 수 있습니다(이는 shell 옵션 및 child_process.exec()이 수행하는 작업입니다). 어떤 경우든 스크립트 파일 이름에 공백이 포함되어 있으면 따옴표로 묶어야 합니다.

.fork()를 사용하여 프로세스 생성

.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);

output3.gif

출력

부모에서 자식으로 메시지를 전달하려면 포크된 객체 자체에서 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()를 사용하여 프로세스 생성

셸 구문을 사용해야 하고 명령에서 예상되는 데이터 크기가 작은 경우 exec 함수가 좋은 선택입니다. 명령에서 생성된 출력을 버퍼링하고 전체 출력 값을 콜백 함수에 전달합니다(반면, spawn은 스트림을 사용하여 데이터를 처리합니다).

exec은 셸을 스폰한 다음 해당 셸 내에서 command를 실행합니다. callback 함수가 제공되면 인자 (error, stdout, stderr)와 함께 호출됩니다. 성공하면 errornull이 됩니다. 오류가 발생하면 errorError의 인스턴스가 됩니다. error.code 속성은 프로세스의 종료 코드가 됩니다. 콜백에 전달된 stdoutstderr 인자는 자식 프로세스의 stdoutstderr 출력을 포함합니다.

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}`)
})

output4.png

출력

흥미로운 반전은 Options 객체에서 몇 가지 설정을 제공하여 exec에 추가할 수 있다는 것입니다. 예를 들어 cwd 옵션을 사용하여 스크립트의 작업 디렉터리를 변경할 수 있습니다. 예를 들어 위의 예제는 다음과 같이 다른 디렉터리에서 실행하도록 만들 수 있습니다.

twist.png

exec 함수는 셸을 사용하여 명령을 실행하므로 여기에서 셸 구문 을 사용하여 직접 셸 파이프 기능을 활용할 수 있습니다

execFile()을 사용하여 프로세스 생성

셸을 사용하지 않고 파일을 실행해야 하는 경우, execFile 함수가 필요합니다. exec 함수와 동일하게 작동하지만 셸을 사용하지 않기 때문에 좀 더 효율적입니다.

const { execFile } = require('node:child_process');
const child = execFile('node', ['--version'], (error, stdout, stderr) => {
  if (error) {
    throw error;
  }
  console.log(stdout);
});

output5.png

출력

Windows에서 .bat 또는 .cmd 파일과 같은 일부 파일은 자체적으로 실행할 수 없습니다. 이러한 파일은 execFile로 실행할 수 없으며 실행하려면 exec 또는 셸이 true로 설정된 상태에서 spawn이 필요합니다.

동기 프로세스 생성

.spawnSync, .execSync.execFileSync 메서드는 동기식이며 Node.js 이벤트 루프를 블로킹하여 스폰된 프로세스가 종료될 때까지 추가 코드의 실행을 일시 중지합니다.

이러한 블로킹 호출은 일반적으로 스크립팅 작업을 단순화하고 시작 시 애플리케이션 구성의 로딩/처리를 단순화하는 데 유용합니다.

언제 무엇을 사용해야 하나요?

when to use.jpeg

자식 프로세스 Abort/Stop/Kill

자식 프로세스를 종료하는 방법은 몇 가지가 있습니다.

  • 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(); // 자식 프로세스를 중지합니다.

자식 프로세스와 부모 프로세스 간의 I/O 처리

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]
})

output6.png

실행 후 출력 로그

스트림 연결

이것은 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)

output7.png

여기서 각 명령의 출력은 다음 명령의 입력이 됩니다.

명령 실행 시 보안

자식 프로세스가 셸에 접근할 수 있도록 허용할 때는 주의해야 합니다. 특히 외부 소스의 동적 입력을 처리할 때 셸 구문을 사용하면 보안 위험을 초래할 수 있습니다. 이는 사용자가 ';' 및 '$'와 같은 셸 구문 문자를 악용하여 악성 명령을 실행할 수 있는 잠재적인 명령 주입 공격의 여지를 남깁니다. 예를 들어, 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.detachedtrue로 설정하면 부모 프로세스가 종료된 후에도 자식 프로세스가 계속 실행될 수 있습니다. 한 번 활성화하면 다시 비활성화할 수 없습니다.
    Windows 이외의 플랫폼에서 options.detachedtrue로 설정하면 자식 프로세스는 새로운 프로세스 그룹과 세션의 리더가 됩니다. 자식 프로세스는 분리 여부와 관계없이 부모가 종료된 후에도 계속 실행될 수 있습니다.

  • 부모의 이벤트 루프에서 자식 프로세스를 참조하면 부모가 종료되지 않습니다. 이 참조를 제거하려면 해당 자식 프로세스에서 .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}`)
})

output8.png

출력

위의 예제는 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()

셸 구문을 사용하고 부모의 표준 IO를 상속받도록 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', {

})

output9.png

출력

spawn이 셸 구문을 이해하지 못해 오류가 발생했습니다.

셸 옵션을 추가해 보겠습니다.

index.js는 다음과 같습니다.

const { spawn } = require('node:child_process')
const ps = spawn('node child_program.js', {
 shell: true
})

output10.png

출력

이제 몇 가지 말할 수 있는 것은 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
})

output11.png

출력

stdio: 'inherit' 옵션 덕분에 코드를 실행할 때 자식 프로세스가 부모 프로세스의 stdin, stdout, 및 stderr를 상속받습니다. 이로 인해 자식 프로세스의 데이터 이벤트 핸들러가 메인 process.stdout 스트림에서 트리거되어 스크립트가 결과를 바로 출력하게 됩니다.

위의 shell: true 옵션 덕분에 exec와 마찬가지로 전달된 명령에서 셸 구문을 사용할 수 있었습니다. 하지만 이 코드를 사용하면 spawn 함수가 제공하는 데이터 스트리밍의 이점을 여전히 누릴 수 있습니다.

profile
FrontEnd Developer

0개의 댓글

관련 채용 정보