본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 27. · 3 Views
Terminal과 Shell 도구 완벽 가이드
프로그래밍에서 터미널과 셸을 다루는 방법을 배웁니다. 명령 실행부터 출력 스트리밍, 타임아웃 관리까지 실무에서 바로 활용할 수 있는 핵심 기법을 다룹니다.
목차
1. 명령 실행
김개발 씨는 Node.js로 배포 자동화 스크립트를 만들고 있었습니다. 서버에서 git pull 명령을 실행하고 싶은데, JavaScript 코드 안에서 어떻게 터미널 명령을 실행할 수 있을까요?
선배에게 물어보니 "child_process 모듈을 써봐"라는 답이 돌아왔습니다.
명령 실행은 프로그램 내부에서 운영체제의 셸 명령을 호출하는 것입니다. 마치 프로그램이 직접 터미널 창을 열어 명령어를 타이핑하는 것과 같습니다.
Node.js에서는 child_process 모듈의 exec, execSync, spawn 함수를 사용하여 이 작업을 수행합니다.
다음 코드를 살펴봅시다.
const { exec, execSync } = require('child_process');
// 동기 방식: 명령이 끝날 때까지 기다림
const result = execSync('ls -la', { encoding: 'utf8' });
console.log('동기 실행 결과:', result);
// 비동기 방식: 콜백으로 결과 처리
exec('git status', (error, stdout, stderr) => {
if (error) {
console.error('에러 발생:', error.message);
return;
}
if (stderr) {
console.error('표준 에러:', stderr);
}
console.log('실행 결과:', stdout);
});
김개발 씨는 입사한 지 3개월 된 주니어 개발자입니다. 오늘 팀장님께서 배포 자동화 스크립트를 만들어보라는 미션을 주셨습니다.
서버에 접속해서 git pull을 실행하고, npm install을 돌리고, 서비스를 재시작하는 일련의 과정을 자동화해야 합니다. "근데 JavaScript에서 터미널 명령을 어떻게 실행하지?" 김개발 씨는 고개를 갸웃거렸습니다.
옆자리 박시니어 씨가 다가와 말했습니다. "Node.js에는 child_process라는 내장 모듈이 있어.
이걸 쓰면 프로그램 안에서 셸 명령을 실행할 수 있지." 그렇다면 child_process란 정확히 무엇일까요? 쉽게 비유하자면, child_process는 마치 비서를 고용하는 것과 같습니다.
여러분이 사장님이라면, 직접 모든 일을 처리하는 대신 비서에게 "이 서류 복사해 와"라고 지시할 수 있습니다. 비서는 복사실에 가서 일을 처리하고 결과물을 가져다 줍니다.
child_process도 마찬가지입니다. 메인 프로그램이 자식 프로세스를 생성하여 명령을 위임하고, 그 결과를 받아오는 것입니다.
Node.js에서 명령을 실행하는 방법은 크게 두 가지가 있습니다. 동기 방식과 비동기 방식입니다.
execSync는 동기 방식입니다. 명령이 완료될 때까지 프로그램 전체가 멈추고 기다립니다.
마치 비서가 복사를 마치고 돌아올 때까지 사장님이 아무 일도 하지 않고 기다리는 것과 같습니다. 코드가 단순해지는 장점이 있지만, 오래 걸리는 명령을 실행하면 프로그램이 멈춰버립니다.
반면 exec는 비동기 방식입니다. 명령을 실행시켜 놓고 메인 프로그램은 다른 일을 계속합니다.
비서가 복사하러 간 사이에 사장님은 다른 업무를 보는 것이지요. 명령이 완료되면 콜백 함수가 호출되어 결과를 처리합니다.
위 코드를 살펴보면, execSync는 결과를 바로 변수에 담을 수 있습니다. encoding 옵션을 'utf8'로 지정하면 Buffer가 아닌 문자열로 결과를 받을 수 있습니다.
exec의 콜백 함수는 세 개의 인자를 받습니다. error는 실행 중 발생한 오류, stdout은 표준 출력, stderr는 표준 에러입니다.
실무에서는 상황에 따라 적절한 방식을 선택해야 합니다. 빌드 스크립트처럼 순차적으로 실행되어야 하는 경우에는 execSync가 편리합니다.
반면 웹 서버에서 명령을 실행할 때는 exec를 사용해야 서버가 멈추지 않습니다. 주의할 점도 있습니다.
사용자 입력을 그대로 명령어에 넣으면 명령어 주입 공격에 취약해집니다. 예를 들어 사용자가 "; rm -rf /"를 입력하면 심각한 문제가 발생할 수 있습니다.
따라서 사용자 입력은 반드시 검증하고 이스케이프 처리해야 합니다. 김개발 씨는 박시니어 씨의 설명을 듣고 고개를 끄덕였습니다.
"아하, 이제 배포 스크립트를 만들 수 있겠네요!"
실전 팁
💡 - execSync는 간단한 스크립트에, exec는 서버 환경에 적합합니다
- 사용자 입력을 명령어에 포함할 때는 반드시 검증과 이스케이프 처리를 하세요
2. 출력 스트리밍
김개발 씨가 만든 배포 스크립트는 잘 동작했지만 한 가지 문제가 있었습니다. npm install처럼 오래 걸리는 명령은 한참 동안 아무 출력도 없다가 갑자기 결과가 한꺼번에 나타났습니다.
실시간으로 진행 상황을 보고 싶은데 방법이 없을까요?
출력 스트리밍은 명령의 실행 결과를 실시간으로 받아보는 기법입니다. 마치 라이브 방송처럼 데이터가 생성되는 즉시 화면에 표시됩니다.
Node.js의 spawn 함수를 사용하면 stdout과 stderr 스트림에 이벤트 리스너를 붙여 실시간 출력을 처리할 수 있습니다.
다음 코드를 살펴봅시다.
const { spawn } = require('child_process');
// spawn으로 명령 실행 - 실시간 스트리밍 지원
const process = spawn('npm', ['install'], {
cwd: '/path/to/project',
shell: true
});
// 표준 출력 스트리밍
process.stdout.on('data', (data) => {
console.log(`[stdout] ${data.toString()}`);
});
// 표준 에러 스트리밍
process.stderr.on('data', (data) => {
console.error(`[stderr] ${data.toString()}`);
});
// 프로세스 종료 이벤트
process.on('close', (code) => {
console.log(`프로세스 종료, 코드: ${code}`);
});
배포 스크립트를 완성한 김개발 씨에게 팀장님이 피드백을 주셨습니다. "스크립트는 잘 동작하는데, npm install 할 때 화면에 아무것도 안 나오니까 불안하네요.
진행 상황을 실시간으로 볼 수 있으면 좋겠어요." 김개발 씨는 다시 박시니어 씨를 찾아갔습니다. "exec를 쓰면 명령이 끝나야 결과가 나오더라고요.
실시간으로 출력을 보려면 어떻게 해야 하나요?" 박시니어 씨가 대답했습니다. "그럴 땐 spawn을 써야 해.
exec와 spawn의 가장 큰 차이가 바로 출력 처리 방식이야." exec와 spawn의 차이를 비유로 설명해 볼까요? exec는 마치 택배와 같습니다.
물건이 다 포장되어 배송이 완료되어야만 받을 수 있습니다. 반면 spawn은 컨베이어 벨트와 같습니다.
물건이 생산되는 대로 바로바로 전달받을 수 있습니다. spawn 함수는 첫 번째 인자로 실행할 명령어를, 두 번째 인자로 인자 배열을 받습니다.
npm install을 실행하려면 spawn('npm', ['install'])처럼 분리해서 전달합니다. options 객체의 cwd로 작업 디렉토리를, shell: true로 셸을 통한 실행을 지정할 수 있습니다.
spawn이 반환하는 객체에는 stdout과 stderr라는 스트림이 있습니다. 이 스트림에 'data' 이벤트 리스너를 붙이면 데이터가 들어올 때마다 콜백이 실행됩니다.
데이터는 Buffer 형태로 전달되므로 toString()으로 문자열로 변환해 주어야 합니다. 'close' 이벤트는 프로세스가 완전히 종료되었을 때 발생합니다.
종료 코드가 0이면 정상 종료, 0이 아니면 오류가 발생했음을 의미합니다. 이 코드를 활용하면 명령 실행의 성공 여부를 판단할 수 있습니다.
실무에서 출력 스트리밍은 여러 곳에서 활용됩니다. CI/CD 파이프라인에서 빌드 로그를 실시간으로 웹 인터페이스에 표시하거나, 대용량 파일 처리 진행 상황을 사용자에게 보여주는 데 유용합니다.
주의할 점이 있습니다. 스트리밍 데이터가 너무 빠르게 들어오면 처리가 밀릴 수 있습니다.
필요하다면 throttle이나 debounce 처리를 고려해야 합니다. 또한 스트림에서 에러가 발생할 수 있으므로 'error' 이벤트 핸들러도 추가하는 것이 좋습니다.
김개발 씨는 spawn으로 스크립트를 수정했습니다. 이제 npm install이 실행되는 동안 패키지가 설치되는 과정을 실시간으로 볼 수 있게 되었습니다.
팀장님도 만족하셨습니다.
실전 팁
💡 - exec는 간단한 명령에, spawn은 오래 걸리는 명령이나 실시간 출력이 필요할 때 사용하세요
- 스트림 데이터는 Buffer이므로 toString()으로 변환하는 것을 잊지 마세요
3. 타임아웃 관리
어느 날 김개발 씨의 배포 스크립트가 멈춰버렸습니다. 서버에서 응답이 없는 명령을 실행했는데, 프로그램이 무한정 대기하고 있었던 것입니다.
박시니어 씨가 물었습니다. "타임아웃 설정은 해뒀어?" 김개발 씨는 그게 뭔지 몰랐습니다.
타임아웃 관리는 명령 실행에 시간 제한을 두는 것입니다. 마치 요리에 타이머를 맞춰두는 것처럼, 정해진 시간 내에 완료되지 않으면 강제로 중단시킵니다.
이를 통해 무한 대기 상태를 방지하고 시스템의 안정성을 높일 수 있습니다.
다음 코드를 살펴봅시다.
const { exec, spawn } = require('child_process');
// exec에서 타임아웃 설정 (밀리초 단위)
exec('npm install', { timeout: 60000 }, (error, stdout, stderr) => {
if (error && error.killed) {
console.error('타임아웃으로 프로세스가 종료되었습니다');
return;
}
console.log('설치 완료:', stdout);
});
// spawn에서 수동 타임아웃 구현
const process = spawn('long-running-command', [], { shell: true });
const timeoutId = setTimeout(() => {
console.log('타임아웃! 프로세스를 종료합니다');
process.kill('SIGTERM');
}, 30000);
process.on('close', () => {
clearTimeout(timeoutId);
console.log('프로세스가 종료되었습니다');
});
그날의 사건은 이랬습니다. 김개발 씨가 배포 스크립트를 실행했는데, 원격 저장소 연결에 문제가 있었습니다.
git fetch 명령이 응답 없이 계속 대기하고 있었고, 스크립트 전체가 멈춰버렸습니다. 30분이 지나도록 아무 반응이 없자 결국 수동으로 프로세스를 종료해야 했습니다.
박시니어 씨가 말했습니다. "네트워크 명령은 언제든지 멈출 수 있어.
그래서 타임아웃을 설정해야 해. 일정 시간이 지나면 자동으로 포기하도록 만드는 거지." 타임아웃은 마치 식당에서 주문 후 대기하는 것과 같습니다.
보통은 음식이 나오기를 기다리지만, 너무 오래 걸리면 "죄송하지만 취소할게요"라고 말하고 자리를 뜹니다. 무한정 기다리는 것은 시간 낭비이자 위험 요소이기 때문입니다.
exec 함수에서는 timeout 옵션으로 간단히 타임아웃을 설정할 수 있습니다. 값은 밀리초 단위입니다.
60000을 지정하면 60초입니다. 타임아웃이 발생하면 프로세스가 강제 종료되고, 콜백의 error 객체에 killed 속성이 true로 설정됩니다.
spawn은 타임아웃 옵션을 직접 지원하지 않습니다. 대신 setTimeout과 kill 메서드를 조합하여 구현합니다.
프로세스를 시작할 때 타이머를 설정하고, 타이머가 만료되면 process.kill()로 프로세스를 종료합니다. 프로세스가 정상 종료되면 clearTimeout으로 타이머를 해제합니다.
kill 메서드에 전달하는 시그널에 대해 알아봅시다. SIGTERM은 "정중하게 종료해 주세요"라는 요청입니다.
프로세스에게 정리할 시간을 줍니다. SIGKILL은 "즉시 종료하라"는 강제 명령입니다.
프로세스가 저항할 수 없습니다. 일반적으로 SIGTERM을 먼저 보내고, 일정 시간 후에도 종료되지 않으면 SIGKILL을 보내는 패턴을 사용합니다.
실무에서 타임아웃 설정은 필수입니다. 특히 네트워크 요청, 외부 API 호출, 원격 서버 접속 등은 언제든지 지연되거나 멈출 수 있습니다.
타임아웃이 없으면 시스템 전체가 멈추는 장애로 이어질 수 있습니다. 적절한 타임아웃 값을 정하는 것도 중요합니다.
너무 짧으면 정상적인 작업도 중단되고, 너무 길면 타임아웃의 의미가 없습니다. 일반적으로 해당 작업의 평균 소요 시간의 2~3배 정도로 설정합니다.
김개발 씨는 모든 명령에 적절한 타임아웃을 추가했습니다. 이제 네트워크 문제가 발생해도 스크립트가 무한정 멈추지 않고 적절히 오류를 처리하게 되었습니다.
실전 팁
💡 - exec는 timeout 옵션을 직접 지원하므로 간단하게 사용할 수 있습니다
- spawn에서는 setTimeout과 kill을 조합하여 타임아웃을 구현하세요
- SIGTERM으로 정중하게 종료를 요청하고, 응답이 없으면 SIGKILL을 사용하세요
4. 안전한 셸 실행기
김개발 씨는 지금까지 배운 내용을 종합하여 실무에서 사용할 수 있는 셸 실행기를 만들어 보기로 했습니다. 타임아웃, 스트리밍, 에러 처리를 모두 갖춘 안전한 도구가 필요했습니다.
박시니어 씨가 코드 리뷰를 해주기로 했습니다.
안전한 셸 실행기는 명령 실행에 필요한 모든 안전장치를 갖춘 유틸리티입니다. 타임아웃으로 무한 대기를 방지하고, 스트리밍으로 실시간 출력을 지원하며, 체계적인 에러 처리로 문제 상황에 대응합니다.
Promise 기반으로 작성하면 async/await와 함께 깔끔하게 사용할 수 있습니다.
다음 코드를 살펴봅시다.
const { spawn } = require('child_process');
function executeCommand(command, args = [], options = {}) {
const { timeout = 30000, cwd = process.cwd() } = options;
return new Promise((resolve, reject) => {
const proc = spawn(command, args, { cwd, shell: true });
let stdout = '', stderr = '';
const timer = setTimeout(() => {
proc.kill('SIGTERM');
reject(new Error(`타임아웃: ${timeout}ms 초과`));
}, timeout);
proc.stdout.on('data', (data) => { stdout += data; });
proc.stderr.on('data', (data) => { stderr += data; });
proc.on('close', (code) => {
clearTimeout(timer);
code === 0
? resolve({ stdout, stderr, code })
: reject(new Error(`종료 코드 ${code}: ${stderr}`));
});
});
}
박시니어 씨가 김개발 씨에게 미션을 주었습니다. "지금까지 배운 걸 종합해서 실무에서 쓸 수 있는 셸 실행기를 만들어 봐.
안전하고 사용하기 쉬워야 해." 김개발 씨는 어떤 기능이 필요할지 정리해 보았습니다. 첫째, 타임아웃으로 무한 대기를 방지해야 합니다.
둘째, 출력을 수집하면서도 필요하다면 스트리밍할 수 있어야 합니다. 셋째, 성공과 실패를 명확히 구분하는 에러 처리가 필요합니다.
넷째, async/await와 함께 사용할 수 있도록 Promise 기반이어야 합니다. 위 코드를 단계별로 살펴보겠습니다.
함수는 command, args, options 세 개의 인자를 받습니다. options에서 timeout과 cwd를 추출하고 기본값을 설정합니다.
기본 타임아웃은 30초입니다. Promise를 반환하므로 호출하는 쪽에서 await를 사용할 수 있습니다.
내부에서 spawn으로 프로세스를 생성하고, stdout과 stderr 변수에 출력을 누적합니다. setTimeout으로 타임아웃 타이머를 설정합니다.
시간이 초과되면 프로세스를 종료하고 reject를 호출합니다. 프로세스가 정상 종료되면 clearTimeout으로 타이머를 해제합니다.
'close' 이벤트에서 종료 코드를 확인합니다. 코드가 0이면 성공이므로 resolve를 호출하고, 0이 아니면 실패이므로 reject를 호출합니다.
resolve에는 stdout, stderr, code를 담은 객체를 전달합니다. 이 함수를 사용하는 방법은 간단합니다.
async 함수 내에서 await executeCommand('git', ['status'])처럼 호출하면 됩니다. try-catch로 감싸면 타임아웃이나 명령 실패를 쉽게 처리할 수 있습니다.
실무에서는 이런 유틸리티 함수를 프로젝트 공통 모듈로 만들어 두면 편리합니다. 모든 곳에서 일관된 방식으로 명령을 실행할 수 있고, 로깅이나 모니터링 로직을 한 곳에서 관리할 수 있습니다.
박시니어 씨는 김개발 씨의 코드를 검토한 후 고개를 끄덕였습니다. "잘 만들었네.
이제 이걸 실제 프로젝트에 적용해 보자."
실전 팁
💡 - Promise 기반으로 만들면 async/await와 자연스럽게 연동됩니다
- 타임아웃, 스트리밍, 에러 처리를 한 곳에 모아 관리하세요
5. 환경 변수 관리
배포 스크립트가 완성되어 갈 무렵, 김개발 씨는 새로운 문제에 부딪혔습니다. 개발 환경과 운영 환경에서 다른 데이터베이스를 사용해야 하는데, 코드에 직접 주소를 적어 넣으면 환경이 바뀔 때마다 수정해야 합니다.
박시니어 씨가 말했습니다. "환경 변수를 사용해야지."
환경 변수는 운영체제 수준에서 프로그램에 전달되는 설정값입니다. 마치 집 주소가 적힌 쪽지를 택배 기사에게 건네주는 것처럼, 프로그램이 시작될 때 필요한 정보를 외부에서 주입합니다.
코드를 수정하지 않고도 환경에 따라 다른 설정을 적용할 수 있습니다.
다음 코드를 살펴봅시다.
const { spawn, exec } = require('child_process');
// 현재 프로세스의 환경 변수 읽기
console.log('현재 NODE_ENV:', process.env.NODE_ENV);
console.log('홈 디렉토리:', process.env.HOME);
// 자식 프로세스에 환경 변수 전달
const child = spawn('node', ['app.js'], {
env: {
...process.env, // 기존 환경 변수 유지
NODE_ENV: 'production', // 새 환경 변수 추가
DB_HOST: 'prod-db.example.com',
API_KEY: 'secret-key-123'
},
shell: true
});
// exec에서도 환경 변수 전달 가능
exec('echo $MY_VAR', {
env: { ...process.env, MY_VAR: 'Hello World' }
}, (err, stdout) => console.log(stdout));
환경 변수 문제는 실무에서 자주 마주치는 상황입니다. 개발할 때는 로컬 데이터베이스를 사용하고, 테스트할 때는 테스트 서버를, 실제 서비스에서는 운영 서버를 사용해야 합니다.
이 정보를 코드에 직접 적으면 환경이 바뀔 때마다 코드를 수정해야 하고, 실수로 운영 서버 정보가 개발 환경에서 사용될 수도 있습니다. 환경 변수는 이 문제를 해결하는 표준적인 방법입니다.
환경 변수를 비유하자면 마치 연극의 소품과 같습니다. 같은 대본이라도 공연 장소에 따라 소품이 달라질 수 있습니다.
지역 공연에서는 간단한 소품을, 대극장에서는 화려한 소품을 사용하죠. 하지만 배우의 연기(코드)는 동일합니다.
Node.js에서 환경 변수는 process.env 객체를 통해 접근합니다. 이 객체에는 현재 프로세스가 가진 모든 환경 변수가 키-값 쌍으로 저장되어 있습니다.
process.env.NODE_ENV는 실행 환경을, process.env.HOME은 홈 디렉토리 경로를 담고 있습니다. 자식 프로세스를 생성할 때는 env 옵션으로 환경 변수를 전달합니다.
이때 중요한 것이 있습니다. env 옵션을 지정하면 기존 환경 변수가 모두 사라지고 지정한 것만 전달됩니다.
따라서 ...process.env로 기존 환경 변수를 복사한 후 새로운 변수를 추가해야 합니다. 위 코드에서 자식 프로세스는 NODE_ENV가 'production'으로, DB_HOST가 'prod-db.example.com'으로 설정됩니다.
자식 프로세스 내에서 process.env.DB_HOST를 읽으면 이 값을 얻게 됩니다. 실무에서는 .env 파일을 많이 사용합니다.
dotenv 같은 라이브러리로 .env 파일을 읽어 process.env에 로드합니다. 개발 환경에서는 .env.development를, 운영 환경에서는 .env.production을 사용하는 식입니다.
단, .env 파일에는 민감한 정보가 포함되므로 절대 버전 관리 시스템에 커밋하면 안 됩니다. 보안에 관한 주의사항도 있습니다.
환경 변수는 자식 프로세스에 자동으로 전달됩니다. API 키나 비밀번호 같은 민감한 정보가 의도치 않게 노출될 수 있습니다.
필요한 경우에만 선택적으로 전달하고, 로그에 환경 변수를 출력하지 않도록 주의해야 합니다. 김개발 씨는 환경 변수를 활용하여 스크립트를 개선했습니다.
이제 같은 코드가 개발, 테스트, 운영 환경에서 각각 다르게 동작합니다. 코드는 하나지만 설정만 바꾸면 됩니다.
박시니어 씨가 마지막으로 덧붙였습니다. "환경 변수를 잘 활용하면 12-Factor App 원칙을 따르는 현대적인 애플리케이션을 만들 수 있어.
설정을 코드와 분리하는 건 정말 중요한 습관이야."
실전 팁
💡 - env 옵션 사용 시 ...process.env로 기존 환경 변수를 유지하세요
- 민감한 정보가 담긴 .env 파일은 절대 버전 관리에 포함하지 마세요
- 운영 환경의 환경 변수는 서버 설정이나 CI/CD 도구에서 관리하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Phase 2 공격 기법 이해와 방어 실전 가이드
웹 애플리케이션 보안의 핵심인 공격 기법과 방어 전략을 실습 중심으로 배웁니다. 인증 우회부터 SQL Injection, XSS, CSRF까지 실제 공격 시나리오를 이해하고 방어 코드를 직접 작성해봅니다.
실시간으로 답변하는 RAG 시스템 만들기
사용자가 질문하면 즉시 답변이 스트리밍되는 RAG 시스템을 구축하는 방법을 배웁니다. 실시간 응답 생성부터 청크별 스트리밍, 사용자 경험 최적화까지 실무에서 바로 적용할 수 있는 완전한 가이드입니다.
프로덕션 배포와 모니터링 완벽 가이드
초급 개발자를 위한 실전 배포 가이드입니다. Docker 컨테이너화부터 클라우드 배포, 모니터링까지 실무에서 바로 쓸 수 있는 노하우를 담았습니다. 베스트셀러 프로그래밍 입문서 스타일로 술술 읽히게 작성했습니다.
AWS Certificate Manager로 HTTPS 인증서 발급 완벽 가이드
AWS Certificate Manager를 사용하여 무료로 SSL/TLS 인증서를 발급받고, 로드 밸런서에 적용하여 안전한 HTTPS 웹 서비스를 구축하는 방법을 초급자도 쉽게 따라 할 수 있도록 단계별로 안내합니다.
Route 53으로 도메인 연결 완벽 가이드
AWS Route 53을 사용하여 도메인을 등록하고 실제 서비스에 연결하는 전 과정을 실무 스토리와 함께 쉽게 배워봅니다. DNS의 기본 개념부터 레코드 설정, ELB 연결까지 초급 개발자도 쉽게 따라할 수 있도록 구성했습니다.