๐Ÿ”ŽCLI ํ”„๋กœ๊ทธ๋žจ ๋งŒ๋“ค๊ธฐ

์„œ๊ฐ€ํฌยท2021๋…„ 12์›” 18์ผ
3

Node.js

๋ชฉ๋ก ๋ณด๊ธฐ
14/15
post-thumbnail

๊ฐ„๋‹จํ•œ ์ฝ˜์†” ๋ช…๋ น์–ด ๋งŒ๋“ค๊ธฐ

1. CLI

CLI(Command Line Interface) ๊ธฐ๋ฐ˜ ๋…ธ๋“œ ํ”„๋กœ๊ทธ๋žจ์„ ์ œ์ž‘ํ•ด๋ณด๊ธฐ

  • ์ฝ˜์†” ์ฐฝ์„ ํ†ตํ•ด์„œ ํ”„๋กœ๊ทธ๋žจ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ™˜๊ฒฝ
  • ๋ฐ˜๋Œ€ ๊ฐœ๋…์œผ๋กœ๋Š” GUI(๊ทธ๋ž˜ํ”ฝ ์œ ์ € ์ธํ„ฐํŽ˜์ด์Šค)๊ฐ€ ์žˆ์Œ
  • ๋ฆฌ๋ˆ…์Šค์˜ ์…ธ์ด๋‚˜ ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”, ๋ช…๋ น ํ”„๋กฌํ”„ํŠธ ๋“ฑ์ด ๋Œ€ํ‘œ์ ์ธ CLI ๋ฐฉ์‹ ์†Œํ”„ํŠธ์›จ์–ด
  • ๊ฐœ๋ฐœ์ž์—๊ฒŒ๋Š” CLI ํˆด์ด ๋” ํšจ์œจ์ ์ผ ๋•Œ๊ฐ€ ๋งŽ์Œ

2. ์ฝ˜์†” ๋ช…๋ น์–ด

๋…ธ๋“œ ํŒŒ์ผ์„ ์‹คํ–‰ํ•  ๋•Œ node [ํŒŒ์ผ๋ช…] ๋ช…๋ น์–ด๋ฅผ ์ฝ˜์†”์— ์ž…๋ ฅํ•จ

  • node๋‚˜ npm. nodemon์ฒ˜๋Ÿผ ์ฝ˜์†”์—์„œ ์ž…๋ ฅํ•˜์—ฌ ์–ด๋– ํ•œ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ช…๋ น์–ด๋ฅผ ์ฝ˜์†” ๋ช…๋ น์–ด๋ผ๊ณ  ๋ถ€๋ฆ„.
  • node์™€ npm ๋ช…๋ น์–ด๋Š” ๋…ธ๋“œ๋ฅผ ์„ค์น˜ํ•ด์•ผ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ
  • nodemon, rimraf๊ฐ™์€ ๋ช…๋ น์–ด๋Š” npm i โ€“g ์˜ต์…˜์œผ๋กœ ์„ค์น˜ํ•˜๋ฉด ๋ช…๋ น์–ด๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ํŒจํ‚ค์ง€ ๋ช…๊ณผ ์ฝ˜์†” ๋ช…๋ น์–ด๋ฅผ ๋‹ค๋ฅด๊ฒŒ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์Œ(sequelize-cli๋Š” sequelize ๋ช…๋ น์–ด ์‚ฌ์šฉ)
  • ์ด๋Ÿฌํ•œ ๋ช…๋ น์–ด๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ด ์žฅ์˜ ๋ชฉํ‘œ

3. ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ํ•˜๊ธฐ

node-cli ํด๋” ์•ˆ์— package.json๊ณผ index.js ์ƒ์„ฑ

  • index.js ์ฒซ ์ค„์˜ ์ฃผ์„์— ์ฃผ๋ชฉ(์œˆ๋„์—์„œ๋Š” ์˜๋ฏธ ์—†์Œ)
  • ๋ฆฌ๋ˆ…์Šค๋‚˜ ๋งฅ ๊ฐ™์€ ์œ ๋‹‰์Šค ๊ธฐ๋ฐ˜ ์šด์˜์ฒด์ œ์—์„œ๋Š” /usr/bin/env์— ๋“ฑ๋ก๋œ node ๋ช…๋ น์–ด๋กœ ์ด ํŒŒ์ผ์„ ์‹คํ–‰ํ•˜๋ผ๋Š” ๋œป

๐Ÿ”ปpackage.json

{
  "name": "node-cli",
  "version": "0.0.1",
  "description": "nodejs cli program",
  "main": "index.js",
  "author": "ZeroCho",
  "license": "ISC",
  "bin": {
    "cli": "./template.js"
  }
}

๐Ÿ”ปindex.js

#!/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);

4. CLI ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ๋งŒ๋“ค๊ธฐ

package.json์— ๋‹ค์Œ ์ค„์„ ์ถ”๊ฐ€

  • bin ์†์„ฑ์ด ์ฝ˜์†” ๋ช…๋ น์–ด์™€ ํ•ด๋‹น ๋ช…๋ น์–ด ํ˜ธ์ถœ ์‹œ ์‹คํ–‰ ํŒŒ์ผ์„ ์„ค์ •ํ•˜๋Š” ๊ฐ์ฒด
  • ์ฝ˜์†” ๋ช…๋ น์–ด๋Š” cli, ์‹คํ–‰ ํŒŒ์ผ์€ index.js

๐Ÿ”ปpackage.json


{
  "name": "node-cli",
  "version": "0.0.1",
  "description": "nodejs cli program",
  "main": "index.js",
  "author": "ZeroCho",
  "license": "ISC",
  "bin": {
    "cli": "./template.js"
  }
}

5. ์ฝ˜์†” ๋ช…๋ น์–ด ์‚ฌ์šฉํ•˜๊ธฐ

npm i -g๋กœ ์„ค์น˜ ํ›„ cli๋กœ ์‹คํ–‰

  • ๋ณดํ†ต ์ „์—ญ ์„ค์น˜ํ•  ๋•Œ๋Š” ํŒจํ‚ค์ง€ ๋ช…์„ ์ž…๋ ฅํ•˜์ง€๋งŒ ํ˜„์žฌ ํŒจํ‚ค์ง€๋ฅผ ์ „์—ญ ์„ค์น˜ํ•  ๋•Œ๋Š” ์ ์ง€ ์•Š์Œ
  • ๋ฆฌ๋ˆ…์Šค๋‚˜ ๋งฅ์—์„œ๋Š” ๋ช…๋ น์–ด ์•ž์— sudo๋ฅผ ๋ถ™์—ฌ์•ผ ํ•  ์ˆ˜๋„ ์žˆ์Œ
  • ์ „์—ญ ์„ค์น˜ํ•œ ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— node_modules๊ฐ€ ์ƒ๊ธฐ์ง€ ์•Š์Œ

6. ๋ช…๋ น์–ด์— ์˜ต์…˜ ๋ถ™์ด๊ธฐ

process.argv๋กœ ๋ช…๋ น์–ด์— ์–ด๋–ค ์˜ต์…˜์ด ์ฃผ์–ด์กŒ๋Š”์ง€ ํ™•์ธ ๊ฐ€๋Šฅ(๋ฐฐ์—ด๋กœ ํ‘œ์‹œ)

  • ์ฝ”๋“œ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์ „์—ญ ์„ค์น˜ํ•  ํ•„์š”๋Š” ์—†์Œ
  • package.json ๋‚ด์šฉ์ด ๋ฐ”๋€Œ๋ฉด ๋‹ค์‹œ ์ „์—ญ ์„ค์น˜ํ•ด์•ผ ํ•จ
  • ๋ฐฐ์—ด์˜ ์ฒซ ์š”์†Œ๋Š” ๋…ธ๋“œ์˜ ๊ฒฝ๋กœ, ๋‘ ๋ฒˆ์งธ ์š”์†Œ๋Š” cli ๋ช…๋ น์–ด์˜ ๊ฒฝ๋กœ, ๋‚˜๋จธ์ง€๋Š” ์˜ต์…˜

๐Ÿ”ปindex.js

#!/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);

7. ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์ž…๋ ฅ ๋ฐ›๊ธฐ

๋…ธ๋“œ ๋‚ด์žฅ ๋ชจ๋“ˆ readline ์‚ฌ์šฉ

  • createInterface ๋ฉ”์„œ๋“œ๋กœ rl ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ฆ
  • process.stdin, process.stdout์€ ๊ฐ๊ฐ ์ฝ˜์†”์„ ํ†ตํ•ด ์ž…๋ ฅ๋ฐ›๊ณ  ์ถœ๋ ฅํ•œ๋‹ค๋Š” ์˜๋ฏธ
  • question ๋ฉ”์„œ๋“œ๋กœ ์งˆ๋ฌธ์„ ํ‘œ์‹œํ•˜๊ณ  ๋‹ต๋ณ€์ด ๋“ค์–ด์˜ค๋ฉด ์ฝœ๋ฐฑ ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋จ
  • ๋‹ต๋ณ€์€ answer ๋งค๊ฐœ๋ณ€์ˆ˜์— ๋‹ด๊น€

๐Ÿ”ปindex.js

#!/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);

8. ์ฝ˜์†” ๋‚ด์šฉ ์ง€์šฐ๊ธฐ

console.clear๋กœ ์ฝ˜์†” ๋‚ด์šฉ ์ง€์šฐ๊ธฐ
ํ”„๋กœ๊ทธ๋žจ ์‹œ์ž‘ ์‹œ์™€, ์ž˜๋ชป๋œ ๋‹ต๋ณ€ ํ›„์— ์ฝ˜์†” ์ง€์›€

๐Ÿ”ปindex.js

#!/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);

9. ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ช…๋ น์–ด ๋งŒ๋“ค๊ธฐ

๐Ÿ”ปtemplate.js

#!/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 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 = () => { // ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ ํ•จ์ˆ˜
  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 dirAnswer = (answer) => { // ๊ฒฝ๋กœ ์„ค์ •
  directory = (answer && answer.trim()) || '.';
  rl.close();
  makeTemplate();
};

const nameAnswer = (answer) => { // ํŒŒ์ผ๋ช… ์„ค์ •
  if (!answer || !answer.trim()) {
    console.clear();
    console.log('name์„ ๋ฐ˜๋“œ์‹œ ์ž…๋ ฅํ•˜์…”์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
    return rl.question('ํŒŒ์ผ๋ช…์„ ์„ค์ •ํ•˜์„ธ์š”. ', nameAnswer);
  }
  name = answer;
  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 {
    makeTemplate();
  }
};
program(); // ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰๋ถ€

10. ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ช…๋ น์–ด ๋งŒ๋“ค๊ธฐ

๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” exist ํ•จ์ˆ˜์™€ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๋Š” mkdirp ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ฆ
program์ด๋ผ๋Š” ํ•จ์ˆ˜๋Š” template.js์˜ ์‹คํ–‰๋ถ€, makeTemplate์€ ์˜ต์…˜์„ ์ฝ์–ด์„œ ์•Œ๋งž์€ ํ…œํ”Œ๋ฆฟ์„ ์ž‘์„ฑํ•ด์ฃผ๋Š” ํ•จ์ˆ˜
์˜ต์…˜์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋™์ž‘์„ ํ•˜๋„๋ก ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
package.json์˜ ๋ช…๋ น์–ด๋ฅผ ๋ฐ”๊ฟ”์ฃผ๊ณ  ์ „์—ญ ์žฌ์„ค์น˜

11. ๋‹จ๊ณ„์  ๋ช…๋ น์–ด ๋งŒ๋“ค๊ธฐ

template.js๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •
๐Ÿ™Œ๋งํฌ์ฐธ๊ณ 

  • https://github.com/ZeroCho/nodejs-book/blob/master/ch14/14.1/node-cli/template.js
  • ์˜ต์…˜์„ ์ž…๋ ฅํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ readline ๋ชจ๋“ˆ๋กœ ๋‹จ๊ณ„์ ์œผ๋กœ ์งˆ๋ฌธ์„ ํ•ด ์˜ต์…˜์„ ์™ธ์šธ ํ•„* ์š”๊ฐ€ ์—†๋„๋ก ํ•จ
  • ์˜ต์…˜์„ ์ž…๋ ฅํ•˜๋Š” ๊ฒฝ์šฐ ์˜ˆ์ „๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋™์ž‘
  • dirAnswer, nameAnswer, typeAnswer๋Š” ๊ฐ๊ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ, ํŒŒ์ผ๋ช…, ํ…œํ”Œ๋ฆฟ ์ข…๋ฅ˜์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ ๋ฐ›๋Š” ํ•จ์ˆ˜(์ฝ”๋“œ์˜ ์ˆœ์„œ๊ฐ€ ์—ญ์ˆœ)

Commander, Inquirer ์‚ฌ์šฉํ•˜๊ธฐ

1. ํŒจํ‚ค์ง€๋กœ ์‰ฝ๊ฒŒ CLI ํ”„๋กœ๊ทธ๋žจ ๋งŒ๋“ค๊ธฐ

npm์—๋Š” CLI ํ”„๋กœ๊ทธ๋žจ์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋งŽ์ด ์ค€๋น„๋˜์–ด ์žˆ์Œ

  • commander(CLI)์™€ inquirer(์‚ฌ์šฉ์ž์™€ ์ƒํ˜ธ์ž‘์šฉ), chalk(์ฝ˜์†”์— ์ปฌ๋Ÿฌ)๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค์–ด ๋ด„
  • 14.1์˜ ํ”„๋กœ๊ทธ๋žจ์„ commander์™€ inquirer๋กœ ์žฌ์ž‘์„ฑํ•  ๊ฒƒ
  • chalk๋Š” ํ„ฐ๋ฏธ๋„์— ์ƒ‰์„ ์ž…ํžˆ๊ธฐ ์œ„ํ•œ ์šฉ๋„

npm i commander inquirer chalk

2. commander ์‚ฌ์šฉํ•˜๊ธฐ

command.js ํŒŒ์ผ ์ž‘์„ฑ

  • version: ํ”„๋กœ๊ทธ๋žจ์˜ ๋ฒ„์ „ ์„ค์ •(--version ๋˜๋Š” -v๋กœ ํ™•์ธ)
  • usage: ํ”„๋กœ๊ทธ๋žจ ์‚ฌ์šฉ ๋ฐฉ๋ฒ• ๊ธฐ์ž…(--help๋กœ ๋˜๋Š” โ€“h๋กœ ํ™•์ธ)
  • command: ๋ช…๋ น์–ด ๋“ฑ๋ก(template ๊ณผ * ๋“ฑ๋กํ•จ)
  • <>๋Š” ํ•„์ˆ˜ ์˜ต์…˜์„ ์˜๋ฏธ
  • []๋Š” ์„ ํƒ ์˜ต์…˜์„ ์˜๋ฏธ
  • description: ๋ช…๋ น์–ด์— ๋Œ€ํ•œ ์„ค๋ช…์„ ์„ค์ •ํ•˜๋Š” ๋ฉ”์„œ๋“œ
  • alias: ๋ช…๋ น์–ด์— ๋Œ€ํ•œ ๋ณ„์นญ
  • option: ๋ช…๋ น์–ด์— ๋Œ€ํ•œ ์˜ต์…˜์„ ๋“ฑ๋ก
  • --์˜ต์…˜ [๊ฐ’] ๋˜๋Š” โ€“์˜ต์…˜ <๊ฐ’> ํ˜•์‹
  • ๋‘ ๋ฒˆ์งธ ์ธ์ž๋Š” ์„ค๋ช…, ์„ธ ๋ฒˆ์งธ ์ธ์ž๋Š” ๊ธฐ๋ณธ๊ฐ’
  • Action: ๋ช…๋ น์–ด๊ฐ€ ์‹คํ–‰๋  ๋•Œ ์ˆ˜ํ–‰ํ•  ๋™์ž‘ ๋“ฑ๋ก
  • parse: process.argv๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ์˜ต์…˜ ๋“ฑ๋ก

๐Ÿ”ปcommand.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
  .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);

3. commander ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ํ•˜๊ธฐ

ํ”„๋กœ๊ทธ๋žจ ์ „์—ญ ์žฌ์„ค์น˜ ํ›„ ์‹คํ–‰ํ•ด๋ณด๊ธฐ

  • -v, -h๋กœ ๋ฒ„์ „, ์„ค๋ช… ํ™•์ธ ๊ฐ€๋Šฅํ•˜๊ณ  ํ•„์ˆ˜ ์˜ต์…˜๋„ ์ž๋™์œผ๋กœ ์ฒดํฌํ•ด์คŒ

4. template.js๋ฅผ ์ปค๋งจ๋”๋กœ ์ „ํ™˜ํ•˜๊ธฐ

command.js ์ˆ˜์ •

  • template.js๋ฅผ ๋ถ™์—ฌ ๋„ฃ์€ ํ›„ ์ฒซ require ๋ถ€๋ถ„๊ณผ ๋ program ๋ถ€๋ถ„๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋จ

๐Ÿ”ปcommand.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
  .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);

5. ์ „ํ™˜๋œ ํŒŒ์ผ ์‹คํ–‰ํ•˜๊ธฐ

๋ช…๋ น์–ด๋“ค์„ ์ปค๋งจ๋” ์‹์œผ๋กœ ์ „ํ™˜ํ•จ

  • ์˜ต์…˜๋“ค์€ ์ˆœ์„œ๋ฅผ ๋ฐ”๊ฟ”์„œ ์ž…๋ ฅํ•ด๋„ ๋จ

6. inquirer ์‚ฌ์šฉํ•˜๊ธฐ

์—ฌ์ „ํžˆ ์˜ต์…˜๋“ค์„ ์™ธ์›Œ์•ผ ํ•˜๋Š” ๋ถˆํŽธํ•จ

  • inquirer๋กœ ์ƒํ˜ธ ์ž‘์šฉ ์ถ”๊ฐ€

๐Ÿ”ปcommand.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
 .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);

7. inquirer API

readline๋ณด๋‹ค ๊ฐ„๊ฒฐํ•ด์ง

  • ์ปค๋งจ๋”์˜ ์•ก์…˜์ด ์‹คํ–‰๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ triggered๊ฐ€ false๋ผ์„œ inquirer๊ฐ€ ์‹คํ–‰๋จ
  • prompt ๋ฉ”์„œ๋“œ๋กœ ์ƒํ˜ธ์ž‘์šฉ ์ฐฝ ๋„์šธ ์ˆ˜ ์žˆ์Œ
    • type: ์งˆ๋ฌธ์˜ ์ข…๋ฅ˜( input, checkbox, list, password, confirm ๋“ฑ)
    • ์˜ˆ์ œ์—์„œ๋Š” input(ํ‰๋ฒ”ํ•œ ๋‹ต๋ณ€), list(๋‹ค์ค‘ ํƒ์ผ), confirm(Yes or No) ์‚ฌ์šฉ
    • name: ์งˆ๋ฌธ์˜ ์ด๋ฆ„, ๋‹ต๋ณ€ ๊ฐ์ฒด ์†์„ฑ๋ช…์œผ๋กœ ์งˆ๋ฌธ์˜ ์ด๋ฆ„์„, ์†์„ฑ ๊ฐ’์œผ๋กœ ์งˆ๋ฌธ์˜ ๋‹ต์„ ๊ฐ€์ง
    • message: ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œ๋˜๋Š” ๋ฌธ์ž์—ด(์—ฌ๊ธฐ์— ์งˆ๋ฌธ์„ ์ ์Œ)
    • choices: type์ด checkbox, list ๋“ฑ์ธ ๊ฒฝ์šฐ ์„ ํƒ์ง€๋ฅผ ๋„ฃ๋Š” ๊ณณ(๋ฐฐ์—ด๋กœ)
    • default: ๋‹ต ์ ์ง€ ์•Š์•˜์„ ๋•Œ ๊ธฐ๋ณธ๊ฐ’
  • ์˜ˆ์ œ์—์„œ๋Š” ์งˆ๋ฌธ ๋„ค ๊ฐœ๋ฅผ ์—ฐ๋‹ฌ์•„ ํ•˜๊ณ  ์žˆ์Œ
  • ์งˆ๋ฌธ์˜ name์ด type, name, directory๋ผ์„œ ๊ฐ๊ฐ์˜ ๋‹ต๋ณ€์ด answers.type, answers.name, answers.director์— ๋“ค์–ด ์žˆ์Œ.

8. chalk

์ฝ˜์†”์— ์ƒ‰์„ ์ถ”๊ฐ€ํ•จ

  • console.log๊ณผ console.error์— chalk ์ ์šฉ
  • ๋ฌธ์ž์—ด์„ chalk ๊ฐ์ฒด์˜ ๋ฉ”์„œ๋“œ๋กœ ๊ฐ์‹ธ๋ฉด ๋จ
  • chalk.green, chalk.yellow, chalk.red ๋“ฑ๋“ฑ
  • chalk.rgb(12, 34, 56)(๋ฌธ์ž์—ด) ๋˜๋Š” chalk.hex(โ€˜#123456โ€™)(ํ…์ŠคํŠธ)๋„ ๊ฐ€๋Šฅ
  • ๋ฐฐ๊ฒฝ์ƒ‰๋„ ์ง€์ • ๊ฐ€๋Šฅํ•ด์„œ chalk.bgGreen, chalk.bgYellow, chalk.bgRgb, chalk.bgHex
  • ๋™์‹œ์— ์ง€์ •ํ•˜๋ ค๋ฉด chalk.red.bgBlue.bold![]

9. ํ”„๋กœ๊ทธ๋žจ์„ ๊ณต์œ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด?

๋งŒ๋“  CLI ํ”„๋กœ๊ทธ๋žจ์„ ๊ณต์œ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด 5์žฅ์˜ ๊ณผ์ •๋Œ€๋กœ npm์— ๋ฐฐํฌํ•˜๋ฉด ๋จ

  • ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ npm i โ€“g <ํŒจํ‚ค์ง€๋ช…>์„ ํ•œ๋‹ค๋ฉด ๋‹ค์šด๋กœ๋“œ ๋ฐ›์•„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ

๐Ÿ˜ƒ์ถœ์ฒ˜๐Ÿ˜ƒ
Node.js ๊ต๊ณผ์„œ - ๊ธฐ๋ณธ๋ถ€ํ„ฐ ํ”„๋กœ์ ํŠธ ์‹ค์Šต๊นŒ์ง€
https://www.inflearn.com/course/%EB%85%B8%EB%93%9C-%EA%B5%90%EA%B3%BC%EC%84%9C/dashboard

0๊ฐœ์˜ ๋Œ“๊ธ€