반복적인 터미널 명령어, 매번 똑같이 치기 귀찮을 때가 많아요. 이럴 때 쉘 스크립트를 만들어두면 아주 편해요. 하지만 Bash 스크립트 문법은 조금 낯설게 느껴질 수 있어요. JavaScript에 익숙하다면 Google의 zx 라이브러리로 Node.js 환경에서 쉘 스크립트를 작성할 수 있어요.
zx가 뭐고 왜 써야 하나요?
Google이 만든 zx는 JavaScript로 쉘 스크립트를 작성할 수 있게 해주는 도구예요. Bash 스크립트의 복잡한 변수 처리나 에러 핸들링을 JavaScript의 친숙한 async/await와 Promise로 깔끔하게 처리할 수 있죠.
Bash 대신 zx를 쓰면 좋은 점:
- 익숙한 JavaScript 문법 사용
- try-catch로 간단한 에러 처리
- npm 패키지 활용 가능
- JSON 파싱이 기본 내장
- 비동기 처리가 자연스러움
zx 설치하고 첫 스크립트 만들기
zx를 사용하려면 먼저 Node.js가 설치되어 있어야 해요. v16 이상 버전을 권장해요. macOS 터미널을 열고 버전을 확인해보세요.
node --version # v16.0.0 이상이어야 해요
이제 npm으로 zx를 전역에 설치해요.
# npm을 사용해 zx를 컴퓨터 전체에서 쓸 수 있도록 설치해요.
npm install -g zx
설치가 끝났다면, 스크립트 파일을 만들 차례에요. 파일 이름은 자유롭지만, 확장자는 .mjs를 사용해요. ES 모듈 문법을 사용하기 위함이에요.
my-script.mjs 라는 이름으로 파일을 만들고 아래 두 줄을 가장 먼저 추가해 주세요.
#!/usr/bin/env zx
import 'zx/globals';
// 앞으로 이 아래에 코드를 작성할 거예요.
첫 번째 줄 #!/usr/bin/env zx는 이 파일을 zx로 실행하라고 알려주는 약속(Shebang )이에요. 두 번째 줄 import 'zx/globals';는 $나 cd같은 zx의 편리한 기능들을 파일 어디서든 바로 쓸 수 있게 해줘요.
쉘 명령어 실행, $ 하나면 충분해요
예를 들어, 현재 폴더의 파일 목록을 보고 싶다면 이렇게 작성해요.
#!/usr/bin/env zx
import 'zx/globals';
// 'ls -a' 명령어를 실행하고 결과를 기다려요.
const result = await $`ls -a`;
// 실행 결과 중에서 표준 출력(stdout) 내용을 화면에 보여줘요.
console.log(result.stdout);
$ 함수는 Promise를 반환하기 때문에 async/await 문법과 아주 잘 맞아요. 명령어 실행 결과는 객체 형태이고, stdout 프로퍼티로 결과 문자열에 접근할 수 있어요.
변수를 포함한 동적 명령어 실행
JavaScript 변수를 쉘 명령어에 자연스럽게 넣을 수 있어요. 따옴표 처리나 이스케이프를 신경 쓸 필요가 없어요.
#!/usr/bin/env zx
import 'zx/globals';
const fileName = 'myfile.txt';
const content = 'Hello from zx!';
// 파일 생성하고 내용 쓰기 - 변수가 자동으로 안전하게 처리돼요
await $`echo ${content} > ${fileName}`;
// 생성된 파일 확인
const result = await $`cat ${fileName}`;
console.log(`파일 내용: ${result.stdout}`);
// Git 브랜치 정보 가져오기
const branch = await $`git branch --show-current`;
console.log(`현재 브랜치: ${branch.stdout.trim()}`);
조금 더 실용적인 스크립트 만들어보기
이제 zx의 여러 기능을 조합해서 간단한 프로젝트 초기 설정 스크립트를 만들어 볼게요. 이 스크립트는 사용자에게 프로젝트 이름을 물어본 뒤, 해당 이름으로 폴더를 만들고 git을 초기화하는 작업을 자동화해요.
#!/usr/bin/env zx
import 'zx/globals';
// $.verbose를 false로 설정하면 실행되는 명령어를 일일이 보여주지 않아요.
// 스크립트가 더 깔끔하게 실행돼요.
$.verbose = false;
try {
// 1. 사용자에게 프로젝트 이름을 물어봐요.
const projectName = await question('새 프로젝트의 이름을 알려주세요: ');
// 만약 사용자가 아무것도 입력하지 않았다면 오류를 발생시켜요.
if (!projectName) {
// chalk 라이브러리가 기본 내장되어 있어 색상있는 글씨를 쉽게 쓸 수 있어요.
console.error(chalk.red('프로젝트 이름은 필수예요.'));
// process.exit(1)은 스크립트가 비정상적으로 종료되었음을 의미해요.
process.exit(1);
}
// 2. 입력받은 이름으로 폴더를 만들어요.
await $`mkdir ${projectName}`;
console.log(chalk.green(`'${projectName}' 폴더를 만들었어요.`));
// 3. 새로 만든 폴더로 이동해요.
cd(projectName);
console.log(chalk.blue(`'${projectName}' 폴더로 이동했어요.`));
// 4. git 저장소를 초기화해요.
await $`git init`;
console.log(chalk.green('git 저장소를 초기화했어요.'));
// 5. README.md 파일을 만들고 내용을 채워요.
const readmeContent = `# ${projectName}\n\n새로운 프로젝트예요.`;
// fs.writeFile로 파일에 내용을 써요. fs도 기본으로 내장되어 있어요.
await fs.writeFile('README.md', readmeContent);
console.log(chalk.green('README.md 파일을 만들었어요.'));
// 6. 프레임워크 선택 옵션 추가
const useFramework = await question('프레임워크를 사용하시겠어요? (y/n): ');
if (useFramework.toLowerCase() === 'y') {
const framework = await question('사용할 프레임워크? (react/vue/next): ');
if (framework === 'react') {
await $`npx create-react-app .`;
} else if (framework === 'vue') {
await $`npm create vue@latest .`;
} else if (framework === 'next') {
await $`npx create-next-app@latest . --ts --tailwind --app`;
}
} else {
// package.json 초기화
await $`npm init -y`;
}
console.log(chalk.yellow('\n✨ 프로젝트 설정이 끝났어요! ✨'));
} catch (error) {
// 스크립트 실행 중 어떤 단계에서든 오류가 발생하면 여기로 와요.
console.error(chalk.red('\n오류가 발생했어요:'));
console.error(error.stderr || error.message);
process.exit(1);
}
디렉터리 이동과 환경 변수 다루기
zx는 cd() 함수로 디렉터리를 쉽게 이동할 수 있어요. Bash와 달리 스크립트 실행이 끝나도 원래 위치로 돌아가지 않아요.
#!/usr/bin/env zx
import 'zx/globals';
// 홈 디렉터리로 이동
cd('~');
console.log(`현재 위치: ${process.cwd()}`);
// 프로젝트 폴더로 이동 후 작업
cd('~/Documents/my-project');
await $`npm install`;
// 환경 변수 설정하기
process.env.NODE_ENV = 'production';
await $`npm run build`;
// 환경 변수를 읽어서 조건부 실행
if (process.env.CI === 'true') {
console.log('CI 환경에서 실행 중이에요');
await $`npm run test:ci`;
} else {
await $`npm test`;
}
에러 처리와 조건부 실행
쉘 명령어가 실패할 수도 있으니 try-catch로 안전하게 처리해요. Bash 스크립트보다 훨씬 직관적이죠.
#!/usr/bin/env zx
import 'zx/globals';
try {
// Git 상태 확인
const status = await $`git status --porcelain`;
if (status.stdout) {
console.log(chalk.yellow('커밋되지 않은 변경사항이 있어요'));
// 사용자에게 확인받기
const shouldCommit = await question('자동으로 커밋할까요? (y/n): ');
if (shouldCommit.toLowerCase() === 'y') {
// 변경사항 자동 커밋
await $`git add .`;
await $`git commit -m "자동 커밋: ${new Date().toLocaleString()}"`;
console.log(chalk.green('✓ 자동 커밋 완료!'));
}
} else {
console.log(chalk.green('모든 파일이 최신 상태예요'));
}
// 원격 저장소로 푸시
await $`git push origin main`;
console.log(chalk.green('✓ 푸시 완료!'));
} catch (error) {
console.log(chalk.red('Git 작업 중 오류가 발생했어요:'), error.message);
// 오류 발생시 대체 작업
await $`git stash`;
console.log(chalk.blue('변경사항을 임시 저장했어요 (stash)'));
}
병렬 처리로 여러 작업 동시 실행하기
JavaScript의 Promise.all을 활용하면 여러 명령어를 동시에 실행할 수 있어요. 시간이 오래 걸리는 작업들을 병렬로 처리하면 전체 실행 시간을 크게 단축할 수 있죠.
#!/usr/bin/env zx
import 'zx/globals';
console.log(chalk.blue('여러 프로젝트 동시 빌드 시작...'));
// 시작 시간 기록
const startTime = Date.now();
// 병렬로 여러 작업 실행
const results = await Promise.all([
$`cd ~/project1 && npm run build`,
$`cd ~/project2 && npm run build`,
$`cd ~/project3 && npm run build`
]);
// 소요 시간 계산
const elapsed = (Date.now() - startTime) / 1000;
console.log(chalk.green(`모든 빌드 완료! (${elapsed}초 소요)`));
// 각 결과 확인
results.forEach((result, index) => {
console.log(`프로젝트${index + 1} 결과:`, result.stdout.slice(0, 100));
});
파일 작업과 JSON 처리
zx는 fs-extra와 같은 유용한 모듈을 기본 제공해요. JSON 파일을 읽고 쓰는 것도 아주 간단해요.
#!/usr/bin/env zx
import 'zx/globals';
// package.json 읽기
const packageJson = await fs.readJson('package.json');
console.log(`프로젝트 이름: ${packageJson.name}`);
console.log(`현재 버전: ${packageJson.version}`);
// 버전 업데이트 (예: 1.0.0 -> 1.0.1)
const versionParts = packageJson.version.split('.');
versionParts[2] = String(Number(versionParts[2]) + 1);
packageJson.version = versionParts.join('.');
// 업데이트된 내용 저장
await fs.writeJson('package.json', packageJson, { spaces: 2 });
console.log(chalk.green(`버전 업데이트 완료: ${packageJson.version}`));
// 백업 파일 생성
await $`cp package.json package.backup.json`;
console.log(chalk.blue('package.json 백업 완료'));
// 여러 JSON 파일 한번에 처리
const configs = await glob('configs/*.json');
for (const configFile of configs) {
const config = await fs.readJson(configFile);
console.log(`${configFile}: ${config.name || 'unnamed'}`);
}
fetch로 API 호출하고 스크립트에 활용하기
zx는 fetch도 기본 제공해서 외부 API와 연동하는 스크립트도 쉽게 만들 수 있어요.
#!/usr/bin/env zx
import 'zx/globals';
// GitHub API로 레포지토리 정보 가져오기
const response = await fetch('https://api.github.com/repos/google/zx');
const repo = await response.json();
console.log(chalk.yellow(`zx 스타 개수: ${repo.stargazers_count} ⭐`));
console.log(chalk.blue(`최근 업데이트: ${repo.updated_at}`));
// API 정보를 활용한 작업
if (repo.stargazers_count > 40000) {
console.log(chalk.green('인기 있는 프로젝트네요!'));
// 아직 클론하지 않았다면 로컬에 클론
const exists = await fs.pathExists('./zx');
if (!exists) {
await $`git clone ${repo.clone_url}`;
console.log('프로젝트를 클론했어요');
}
}
// 날씨 API 호출 예제
const city = 'Seoul';
const weatherResponse = await fetch(`https://wttr.in/${city}?format=j1`);
const weather = await weatherResponse.json();
console.log(`${city} 현재 온도: ${weather.current_condition[0].temp_C}°C`);
실무에서 바로 쓸 수 있는 배포 스크립트
실제 프로젝트에서 자주 쓰는 배포 자동화 스크립트예요. 테스트, 빌드, 배포까지 한 번에 처리해요.
#!/usr/bin/env zx
import 'zx/globals';
// 배포 전 체크리스트
console.log(chalk.blue('🚀 배포 프로세스 시작...'));
// 컬러풀한 스피너로 진행 상황 표시
const spinner = (message) => {
console.log(chalk.cyan(`⏳ ${message}...`));
};
try {
// 1. 테스트 실행
spinner('테스트 실행 중');
await $`npm test`;
console.log(chalk.green('✓ 테스트 통과'));
// 2. 린트 검사
spinner('코드 스타일 검사 중');
await $`npm run lint`;
console.log(chalk.green('✓ 린트 통과'));
// 3. 빌드
spinner('프로젝트 빌드 중');
await $`npm run build`;
console.log(chalk.green('✓ 빌드 완료'));
// 4. Git 상태 확인
const gitStatus = await $`git status --porcelain`;
if (gitStatus.stdout) {
await $`git add .`;
await $`git commit -m "배포: v${new Date().getTime()}"`;
console.log(chalk.green('✓ 변경사항 커밋'));
}
// 5. 태그 생성
const version = `v${new Date().toISOString().split('T')[0]}`;
await $`git tag ${version}`;
await $`git push origin main --tags`;
console.log(chalk.green(`✓ 태그 생성: ${version}`));
// 6. 서버에 배포 (rsync 사용)
spinner('서버에 파일 업로드 중');
await $`rsync -avz ./dist/ user@server:/var/www/html/`;
// 7. 서버 재시작 (SSH 명령)
await $`ssh user@server "pm2 restart app"`;
console.log(chalk.green.bold(`\n🎉 배포 완료! 버전: ${version}`));
// 8. 배포 알림 (Slack webhook 예제)
await fetch('https://hooks.slack.com/services/YOUR/WEBHOOK/URL', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `배포 완료! 🚀 버전: ${version}`
})
});
} catch (error) {
console.log(chalk.red('✗ 배포 실패!'));
console.error(error.message);
// 실패 시 롤백
const rollback = await question('이전 버전으로 롤백할까요? (y/n): ');
if (rollback.toLowerCase() === 'y') {
await $`git reset --hard HEAD~1`;
await $`git push --force`;
console.log(chalk.yellow('이전 버전으로 롤백했어요'));
}
process.exit(1);
}
출력 조절과 디버깅 팁
스크립트를 디버깅하거나 출력을 조절하고 싶을 때 유용한 팁들이에요.
#!/usr/bin/env zx
import 'zx/globals';
// verbose 모드 끄기 (명령어 출력 숨김)
$.verbose = false;
// 조용히 실행하고 결과만 처리
const files = await $`ls -la`;
console.log('파일 개수:', files.stdout.split('\n').length);
// 특정 부분만 verbose 켜기
$.verbose = true;
console.log(chalk.yellow('다음 명령어는 실행 과정을 보여줘요:'));
await $`echo "이 명령어는 보여요"`;
$.verbose = false;
// 실행 시간 측정
const start = Date.now();
await $`sleep 2`;
console.log(`실행 시간: ${Date.now() - start}ms`);
// 명령어 실패해도 계속 진행 (nothrow)
const result = await $`exit 1`.nothrow();
if (result.exitCode !== 0) {
console.log('명령어가 실패했지만 계속 진행해요');
}
// 타임아웃 설정
try {
await $`sleep 10`.timeout('3s');
} catch {
console.log('타임아웃! 3초 이상 걸려서 중단했어요');
}
유용한 내장 함수들
zx가 제공하는 다른 유용한 함수들도 알아볼게요.
#!/usr/bin/env zx
import 'zx/globals';
// sleep - 일정 시간 대기
console.log('3초 기다려요...');
await sleep(3000);
console.log('계속 진행!');
// which - 실행 파일 경로 찾기
const nodePath = await which('node');
console.log(`Node.js 경로: ${nodePath}`);
// glob - 파일 패턴 매칭
const jsFiles = await glob('**/*.js');
console.log(`JavaScript 파일 ${jsFiles.length}개 발견`);
// os 정보 활용
console.log(`운영체제: ${os.platform()}`);
console.log(`CPU 코어: ${os.cpus().length}개`);
// 임시 디렉터리 생성
const tmpDir = await $`mktemp -d`;
console.log(`임시 폴더: ${tmpDir.stdout.trim()}`);
cd(tmpDir.stdout.trim());
// 작업 수행...
await $`rm -rf ${tmpDir.stdout.trim()}`;
스크립트 실행하기
작성한 스크립트를 실행하는 방법은 두 가지예요.
방법 1: 실행 권한 주고 직접 실행
가장 정석적인 방법이에요. 파일에 실행 권한을 한 번만 부여하면, 그 다음부터는 파일 이름만으로 실행할 수 있어요.
# my-script.mjs 파일에 실행 권한(+x)을 줘요.
chmod +x my-script.mjs
# 이제 파일을 직접 실행할 수 있어요.
./my-script.mjs
방법 2: zx 명령어로 실행
매번 실행 권한을 주기 번거롭다면 zx 명령어로 바로 실행할 수도 있어요.
zx my-script.mjs
두 방법 모두 동일하게 작동해요. 반복적으로 사용하는 스크립트라면 첫 번째 방법을, 가끔 한 번씩 실행하는 스크립트라면 두 번째 방법이 편할 수 있어요.
프로를 위한 고급 팁
1. 커스텀 에러 메시지
// 에러를 더 친근하게 표시
process.on('uncaughtException', (error) => {
console.error(chalk.red('앗! 예상치 못한 오류가 발생했어요 😱'));
console.error(chalk.yellow(error.message));
process.exit(1);
});
2. 설정 파일 활용
// .zxrc.mjs 파일을 만들어 기본 설정 관리
export default {
verbose: false,
shell: '/bin/zsh',
prefix: 'set -euo pipefail;',
quote: (s) => `'${s.replace(/'/g, "'\\''")}'`
};
3. 재사용 가능한 함수 모듈화
// utils.mjs
export async function gitCommit(message) {
await $`git add .`;
await $`git commit -m ${message}`;
console.log(chalk.green(`✓ 커밋: ${message}`));
}
// main.mjs
import { gitCommit } from './utils.mjs';
await gitCommit('기능 추가');
마무리
zx를 사용하면 복잡한 따옴표 처리나 변수 전달 문제를 신경 쓰지 않고도 JavaScript의 유연함으로 쉘 스크립트를 만들 수 있어요. Bash의 제한적인 문법에서 벗어나 JavaScript의 모든 기능을 활용할 수 있다는 게 가장 큰 장점이죠.
특히 비동기 처리, 에러 핸들링, JSON 파싱, API 호출 같은 작업이 훨씬 직관적이 되어요. 반복적인 작업을 자동화하고 싶을 때 zx로 나만의 스크립트를 만들어 보세요. 작업 효율이 크게 올라갈 거예요.