
CLI(Command Line Interface) : 콘솔 창을 통해 프로그램을 수행하는 환경
노드는 서버가 아니고, JavaScript를 브라우저 외 환경에서 돌릴 수 있게 해주는 런타임
근데 지금까지는 서버로만 만들었음
-> 터미널 명령어 기반으로 동작하는 노드 프로그램 생성 실습
node, npm 명령어 - 노드 설치해야만 사용 가능
nodemon, rimraf와 같은 명령어는 해당 패키지를 npm을 통해 전역 설치하여 콘솔에서 명령어로 사용 가능
{
"name": "node-cli",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Zerocho",
"license": "ISC",
"description": "nodejs cli program",
"bin" : {
"cli": "./index.js"
}
}
#!/usr/bin/env node
console.log("Hello CLI!", process.argv);
npm i -g
콘솔에서 npx cli로 확인한 결과

두 명령어는 각각 node와 index.js의 경로
콘솔에서 npx cli에 명령어 붙여 호출

readline : 사용자로부터의 입력 받는 모듈
#!/usr/bin/env node
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.clear();
const answerCallback = (answer) => {
if (answer === 'y') {
console.log('감사합니다!');
rl.close();
} else if (answer === 'n') {
console.log('죄송합니다!');
rl.close();
} else {
console.clear();
console.log('y 또는 n 만 입력 가능합니다.');
rl.question('예제가 재미있습니까? (y/n): ', answerCallback);
}
};
rl.question('예제가 재미있습니까? (y/n): ', answerCallback);
실행하면 다음과 같다.
예제
1. 명령어 입력npx cli html main ./public/html
- public/html 안에 main.html 생성
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const type = process.argv[2];
const name = process.argv[3];
const directory = process.argv[4] || '.';
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Templete</title>
</head>
<body>
<h1>Hello</h1>
<p>CLI</p>
</body>
</html>
`;
const routerTemplate = `
const express = require('express');
const router = express.Router();
router.get('/', (req, res, next) => {
try {
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
`;
const exist = (dir) => { // 폴더 존재 확인 함수
try {
fs.accessSync(dir, fs.constants.F_OK || fs.constants.R_OK || fs.constants.W_OK);
return true;
} catch (e) {
return false;
}
};
const mkdirp = (dir) => { // 경로 생성 함수
const dirname = path
.relative('.', path.normalize(dir))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if (!exist(pathBuilder)) {
fs.mkdirSync(pathBuilder);
}
});
};
const makeTemplete = () => {
mkdirp(directory);
if (type === 'html') {
const pathToFile = path.join(directory, `${name}.html`);
if(exist(pathToFile)) {
console.error('이미 존재하는 파일입니다.');
} else
fs.writeFileSync(pathToFile, htmlTemplate);
console.log(pathToFile, '생성완료');
} else if (type === 'express-router') {
const pathToFile = path.join(directory, `${name}.js`);
if (exist(pathToFile)) {
console.error('이미 존재하는 파일입니다.');
} else {
fs.writeFileSync(pathToFile, routerTemplate);
console.log(pathToFile, '생성완료');
}
} else {
console.error('html 또는 express-router 타입만 생성할 수 있습니다.');
}
};
const program = () => {
if (!type || !name) {
console.error('사용법: cli html|express-router 파일명 [경로]');
} else {
makeTemplete();
}
};
program(); // 실행
{
...
"bin" : {
"cli": "./template.js"
}
}
package.json을 수정했으므로 새로 전역 설치 필요
npm i -g
콘솔에 npx cli를 입력하여 실행한다.
실행 함수가 templete.js로 바뀌지 않고 index.js가 계속 나온다면
npm ls -g node-cli
이 경로로 이동하여 cli 명령어를 삭제할 수 있는 명령어를 입력한다.npx rimraf cli climcmd climps1이후 다시
npm i -g로 패키지를 설치해 준다.
npx cli 명령어를 이용해서 디렉터리와 파일을 생성할 수 있다.
npx cli 명령어를 이용해 public/html 파일과 routes/index.js 파일을 생성할 수 있다.
CLI 프로그램이 사용자가 명령어를 순서대로 사용할 수 있도록 질문을 하도록 코드를 수정한다.
<질문 순서>
1. 어떤 템플릿을 생성할지 설정 (html / express-router)
2. 파일명 설정
3. 저장경로 설정
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const readline = require('readline');
let rl;
let type = process.argv[2];
let name = process.argv[3];
let directory = process.argv[4] || '.';
...
const dirAnswer = (answer) => {
directory = answer?.trim() || '.';
rl.close();
makeTemplete();
};
const nameAnswer = (answer) => {
if (!answer || !answer.trim()) {
console.clear();
console.log('name을 반드시 입력하셔야 합니다.');
return rl.question('파일명(확장자 제외)을 입력하세요: ', nameAnswer);
}
name = answer.trim();
return rl.question('경로를 입력하세요(기본값: 현재경로): ', dirAnswer);
}
const typeAnswer = (answer) => {
if (answer !== 'html' && answer !== 'express-router') {
console.clear();
console.log('html 또는 express-router 타입만 생성할 수 있습니다.');
return rl.question('어던 템플릿이 필요하십니까? ', typeAnswer);
}
type = answer;
return rl.question('파일명(확장자 제외)을 입력하세요: ', nameAnswer);
};
const program = () => {
if (!type || !name) {
rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.clear();
rl.question('어던 템플릿이 필요하십니까? ', typeAnswer);
} else {
makeTemplete();
}
};
program(); // 실행
추가된 함수의 순서는 실제 실행 순서의 역순이다.
실행하면 결과는 다음과 같다.
CLI 프로그램 삭제 방법
npm 전역 제거 명령어 호출npm rm -g또는
npm rm -g node-cli
commander를 사용해 위에서 만든 CLI 프로그램 재연
commander 9버전, inquirer 8버전을 사용한다.
npm i commander@9 inquirer@8
command.js
#!/usr/bin/env node
const { program } = require('commander');
program
.version('0.0.1', '-v, --version')
.name('cli');
program
.command('template <type>')
.usage('<type> --filename [filename] --path [path]')
.description('템플릿을 생성합니다')
.alias('tmpl')
.option('-f, --filename [filename]', '파일명을 입력하세요.', 'index')
.option('-d, --directory [directory]', '생성 경로를 입력하세요.', '.')
.action((type, options, command) => {
console.log(type, options.filename, options.directory);
});
program
.command('*', { noHelp: true })
.action(() => {
console.error('알 수 없는 명령어입니다.');
program.help();
});
program
.parse(process.argv);
commander 객체 중 program 객체 호출
program 메서드
cli template html 이런 식으로 명령어 사용그리고 package.json의 bin 속성에 새로 만든 command.js 파일을 연결한다.
package.json
{
"bin" : {
"cli": "./command.js"
}
}
명령어로 전역 설치를 진행한다.
npm i -g
commander가 기본적으로 제공하는 -v와 -h를 테스트하면 다음과 같다.
<type>을 작성하지 않았기 때문에 에러 발생template.js에 있는 코드 중 htmlTemplate, routerTemplate, exist(), mkdirp(), makeTemplate()를 가져와서 작성해 준다.
#!/usr/bin/env node
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Template</title>
</head>
<body>
<h1>Hello</h1>
<p>CLI</p>
</body>
</html>
`;
const routerTemplate = `
const express = require('express');
const router = express.Router();
router.get('/', (req, res, next) => {
try {
res.send('ok');
} catch (error) {
console.error(error);
next(error);
}
});
module.exports = router;
`;
const exist = (dir) => {
try {
fs.accessSync(dir, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK);
return true;
} catch (e) {
return false;
}
};
const mkdirp = (dir) => {
const dirname = path
.relative('.', path.normalize(dir))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if(!exist(pathBuilder)) {
fs.mkdirSync(pathBuilder);
}
});
};
const makeTemplate = (type, name, directory) => {
mkdirp(directory);
if (type === 'html') {
const pathToFile = path.join(directory, `${name}.html`);
if(exist(pathToFile)) {
console.error('이미 해당 파일이 존재합니다:');
} else {
fs.writeFileSync(pathToFile, htmlTemplate);
console.log(pathToFile, '생성 완료');
}
} else if (type === 'express-router') {
const pathToFile = path.join(directory, `${name}.js`);
if(exist(pathToFile)) {
console.error('이미 해당 파일이 존재합니다:');
} else {
fs.writeFileSync(pathToFile, routerTemplate);
console.log(pathToFile, '생성 완료');
}
} else {
console.error('html 또는 express-router 타입만 생성 가능합니다.');
}
}
program
.version('0.0.1', '-v, --version')
.name('cli');
program
.command('template <type>')
.usage('<type> --filename [filename] --path [path]')
.description('템플릿 생성합니다')
.alias('tmpl')
.option('-f, --filename [filename]', '파일명을 입력하세요.', 'index')
.option('-d, --directory [directory]', '생성 경로를 입력하세요.', '.')
.action((type, options, command) => {
makeTemplate(type, options.filename, options.directory); // 수정
});
program
.command('*', { noHelp: true })
.action(() => {
console.error('알 수 없는 명령어입니다.');
program.help();
});
program
.parse(process.argv);
테스트해보면 다음과 같다.
-d public/html과 -f new는 순서 상관xCLI 프로그램이 단계적 질문을 하던 것처럼, inquirer 객체를 사용해 프로그램이 사용자에게 단계적 선택지를 제공하도록 만들 수 있다.
#!/usr/bin/env node
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
...
program
.command('template <type>')
.usage('<type> --filename [filename] --path [path]')
.description('템플릿 생성합니다')
.alias('tmpl')
.option('-f, --filename [filename]', '파일명을 입력하세요.', 'index')
.option('-d, --directory [directory]', '생성 경로를 입력하세요.', '.')
.action((type, options) => {
makeTemplate(type, options.filename, options.directory);
});
program
.action((options, command) => {
if(command.args.length !== 0) {
console.log('해당 명령어를 찾을 수 없습니다.');
program.help();
} else {
inquirer.prompt([{
type: 'list',
name: 'type',
message: '템플릿 종류를 선택하세요.',
choices: ['html', 'express-router'],
}, {
type: 'input',
name: 'name',
message: '파일명을 입력하세요.',
default: 'index',
}, {
type: 'input',
name: 'directory',
message: '생성 경로를 입력하세요.',
default: '.',
}, {
type: 'confirm',
name: 'confirm',
message: '생성하시겠습니까?',
}])
.then((answers) => {
if (answers.confirm) {
makeTemplate(answers.type, answers.name, answers.directory);
console.log(chalk.rgb(128, 128, 128)('터미널을 종료합니다.')); // 추가
}
});
}
})
.parse(process.argv);
chalk 패키지 : 터미널에 색과 스타일 추가
npm i chalk@4
#!/usr/bin/env node
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const chalk = require('chalk');
...
const makeTemplate = (type, name, directory) => {
mkdirp(directory);
if (type === 'html') {
const pathToFile = path.join(directory, `${name}.html`);
if(exist(pathToFile)) {
console.error(chalk.bold.red('이미 해당 파일이 존재합니다:')); // 추가
} else {
fs.writeFileSync(pathToFile, htmlTemplate);
console.log(chalk.green(pathToFile, '생성 완료')); // 추가
}
} else if (type === 'express-router') {
const pathToFile = path.join(directory, `${name}.js`);
if(exist(pathToFile)) {
console.error(chalk.bold.red('이미 해당 파일이 존재합니다:')); // 추가
} else {
fs.writeFileSync(pathToFile, routerTemplate);
console.log(chalk.green(pathToFile, '생성 완료')); // 추가
}
} else {
console.error(chalk.bold.red('html 또는 express-router 타입만 생성 가능합니다.')); // 추가
}
}
program
.version('0.0.1', '-v, --version')
.name('cli');
program
.command('template <type>')
.usage('<type> --filename [filename] --path [path]')
.description('템플릿 생성합니다')
.alias('tmpl')
.option('-f, --filename [filename]', '파일명을 입력하세요.', 'index')
.option('-d, --directory [directory]', '생성 경로를 입력하세요.', '.')
.action((type, options) => {
makeTemplate(type, options.filename, options.directory);
});
program
.action((options, command) => {
if(command.args.length !== 0) {
console.log(chalk.bold.red('해당 명령어를 찾을 수 없습니다.')); // 추가
program.help();
} else {
inquirer.prompt([{
type: 'list',
name: 'type',
message: '템플릿 종류를 선택하세요.',
choices: ['html', 'express-router'],
}, {
type: 'input',
name: 'name',
message: '파일명을 입력하세요.',
default: 'index',
}, {
type: 'input',
name: 'directory',
message: '생성 경로를 입력하세요.',
default: '.',
}, {
type: 'confirm',
name: 'confirm',
message: '생성하시겠습니까?',
}])
.then((answers) => {
if (answers.confirm) {
makeTemplate(answers.type, answers.name, answers.directory);
console.log(chalk.rgb(128, 128, 128)('터미널을 종료합니다.')); // 추가
}
});
}
})
.parse(process.argv);
chalk 패키지 사용법 : chalk 객체의 메서드들로 문자열을 감싼다.
직접 색 지정 -> rgb(128, 128, 128)메서드나 hex(#123fff) 메서드 사용
그 외
- 배경색 지정 : bgGreen, byYellow나 bgRgb, bgHex 등 사용
- 텍스트 스타일 지정 : bold, underline 등 사용
- 글자색, 배경색, 스타일 동시 지정 : chalk.red.bgBlue, bold(텍스트) 처럼 사용
색상이 적용된 CLI 프로그램의 콘솔은 다음과 같다.
![]() | ![]() | ![]() |
|---|
핵심 정리