commander, inquirer 사용하기(2)
commander가 기본적으로 제공하는 옵션인 -v와 -h를 입력해보겠습니다.
$ cli -v
$ cli -h
$ npx cli template -h
$ npx cli template
명령어에 -h 옵션을 붙이면 명령어 설명서가 나옵니다. 조금 전에 usage나 name, description, option 메서드에 적었던 설명이 그대로 표시됩니다.
cli template 명령어를 입력했을 때는 필수 요소인 <type>을 빠뜨렸으므로 에러를 표시합니다. 이렇게 설명과 에러 검증을 자동으로 해줘서 편리합니다.
이제 실제로 동작하는 코드를 작성해보겠습니다. template.js의 코드를 대부분 가져옵니다.
#!/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 htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta chart="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(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 [path]", "생성 경로를 입력하세요", ".")
.action((type, options) => {
makeTemplate(type, options.filename, options.directory);
});
program.command("*", { noHelp: true }).action(() => {
console.log("해당 명령어를 찾을 수 있습니다.");
program.help();
});
program.parse(process.argv);
콘솔에서 실행해보면 됩니다. template.js 프로그램과 명령어만 다를 뿐 동일하게 동작합니다.
$ cli template html -d public/html -f new
$ cli copy
$ cli
옵션들은 순서를 바꿔서 입력해도 됩니다. -d public/html -f new나 -f new -d public/html이나 똑같급니다. cli copy처럼 미리 등록하지 않은 명령어를 사용하면 * 와일드카드 명령어가 실행됩니다.
commander에서 알아두어야 할 것은 기번 명렁어(cli)는 * 명령어에 해당하지 않는다는 것입니다. 이 명령어에 동작을 추가하려면 * 명령어를 없애고 일반 action에서 매개변수에 따라 분기 처리해야 합니다.
commander를 사용하더라도 여전히 명령어를 외워야 합니다. 설명서도 제공하고 옵션 순서도 바꿀 수 있지만, 불편한 것이 사실입니다. 따라서 inquirer로 cli 명령어를 사용할 때 사용자와 상호작용할 수 있도록 만들어봅시다.
#!/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 htmlTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta chart="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(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 [path]", "생성 경로를 입력하세요", ".")
.action((type, options) => {
makeTemplate(type, options.filename, options.directory);
});
program
.action((cmd, args) => {
if (args) {
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);
readline 모듈을 사용할 때는 엄청 복잡했던 코드가 간결해졌습니다(inquirer 패키지도 내부적으 로 readline 모듈을 사용하기는 합니다).* command를 없애고 progran 객체에 바로 action 메서 드를 붙입니다. 매개변수로 Cmd와 args가 들어오는데, 첫 번째 매개변수인 cmd에는 명령어에 대한 전체적인 내용이 들어 있고 두 번째 매개변수인 args에는 cli 명령어 다음에 오는 인수가 들어 있 습니다. 만약 명령어가 cLi Copy면 'copy']가 들어 있고, 명령어가 cl1면 undef ined가 들어 있습 니다. 따라서 args 값의 유무로 cLi를 입력했는지 입력하지 않았는지를 구별할 수 있습니다.
이제 cli를 입력하면 사용자와의 상호작용을 시작합니다. inquirer 패키지로부터 불러온 inquirer 객체는 prompt라는 메서드를 가지고 있습니다. 이 메서드는 인수로 질문 목록을 받고, 프로미스를 통해 답변(answers 객체)을 반환합니다. 질문 객체의 속성을 알아봅시다.
- type: 질문의 종류입니다. input, checkbox, list, password, Conticm 능이 있습니다. 이 예 제에서는 input(평범한 답변), list(다중 택일), Confirm(Yes 또는 No)과 같은 종류의 질문 을 사용합니다.
- name : 질문의 이름입니다. 나중에 답변 객체가 속성명으로 질문의 이름을, 속성값으로 질문 의 답을 가지게 됩니다.
- message : 사용자에게 표시되는 문자열입니다. 여기에 실제 질문을 적으면 됩니다.
- choices: type이 checkbox, 1ist 등인 경우 선택지를 넣는 곳입니다. 배열로 넣으면 됩니다.
- default : 답을 적지 않았을 경우 적용되는 기본값입니다.
예제에서는 질문 네 개를 연달아 합니다. 질문 객체 네 개를 배열로 묶어 prompt 메서드의 인수로 제공했습니다. prompt 메서드는 프로미스를 반환하므로 then 메서드를 붙여 답변을 매개변수를 통해 받을 수 있습니다.
콘솔에 명령어를 입력해보면 훨씬 더 풍부한 상호작용을 하는 것을 볼 수 있습니다.
$ npx cli
list 타입의 질문은 키보드 화살표를 통해 답변을 고를 수 있습니다. 답변 선택기는 choices 속성에 넣어준 것들입니다. 계속해서 나머지 질문을 진행하면 다음과 같은 결과가 나옵니다.
? 템플릿 종류는 선택하세요. html
? 파일의 이름을 입력하세요. new
? 파일이 위치할 폴더의 경로를 입력하세요. public/html
? 생성하시겠습니까? y
이미 해당 파일이 존재합니다.
터미널을 종료합니다.
여기에 입력했던 답변들은 answers 객체에 저장되어 프로미스를 통해 반환됩니다. 질문 객체에 넣어줬던 name 속성과 질문의 답변이 각각 키와 값이 됩니다. 예를 들면 첫 번째 질문의 name이 type이므로 answers.type == 'html'이 되는 것입니다.
'프로그래밍 언어 > NODE JS' 카테고리의 다른 글
| AWS와 GCP로 배포하기 - 서비스 운영을 위한 패키지 (0) | 2026.04.06 |
|---|---|
| commander, inquirer 사용하기(3) (0) | 2026.04.03 |
| commander, inquirer 사용하기(1) (0) | 2026.03.28 |
| 간단한 콘솔 명령어 만들기(5) (0) | 2026.03.25 |
| 간단한 콘솔 명령어 만들기(4) (0) | 2026.03.22 |