본문 바로가기

BackEnd/Node

[노드교과서] 섹션 13. 14장 CLI 프로그램 만들기

14.1 간단한 콘솔 명령어 만들기

 1. CLI

  ⇒ CLI(Command Line Interface) 기반 노드 프로그램을 제작해보기

   - 콘솔 창을 통해 프로그램을 수행하는 환경을 구현하며 반대 개념은 GUI(그래픽 유저 인터페이스)이다.

   - 리눅스의 셸이나 브라우저 콘솔, 명령 프롬프트 등이 대표적인 CLI 방식 소프트웨어이다.

 2. 콘솔 명령어

  ⇒ 노드 파일을 실행할 때 node[파일명] 명령어를 콘솔에 입력한다.

   - node나 npm, nodemon처럼 콘솔에서 입력해 어떠한 동작을 수행하는 명령어를 콘솔 명령어라고 한다.

   - nodemon, rimraf같인 명렁어는 npm i -g 옵션으로 설치하면 명령어로 사용이 가능하다.

   - 패키지 명과 콘솔 명령어를 다르게 만들 수도 있다.(sequelize-cli는 sequelize 명령어 사용)

 3. 프로젝트 시작하기

  ⇒ node-cli 폴더 안 packate.json과 index.js 파일을 생성한다.

// package.json

{
  "name": "cli",
  "version": "0.0.1",
  "description": "nodejs cli program",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bin": {
    "cli": "./index.js"
  },
}

 

 

#!/usr/bin/env node
// cli linux나 max에서 node로 index.js를 실행해라라는 문법
//Hello CLI [
//   'C:\\Program Files\\nodejs\\node.exe',   node 명령어의 경로
//   'C:\\Users\\user\\AppData\\Local\\npm-cache\\_npx\\9a59ee6893e4192e\\node_modules\\cli\\index.js'  실제 파일의 경로
// ]
console.log('Hello CLI', process.argv);

 

 6. 명령어에 옵션 붙이기

  ⇒ process.argv로 명령어에 어떤 옵션이 주어졌는지 확인이 가능하다.(배열로 표시)

   - 코드가 바뀔때마다 전역 설치할 필요는 없으나 package.json 내용이 바뀌면 다시 전역 설치해야 한다.

   - 배열의 첫요소는 노드의 경로, 두 번째 요소는 cli명령어의 경로, 나머지는 옵션

C:\nodejs\cli>cli one two three four
Hello CLI [
  'C:\\Program Files\\nodejs\\node.exe',
  'C:\\Users\\user\\AppData\\Roaming\\npm\\node_modules\\cli\\index.js',
  'one',
  'two',
  'three',
  'four'
]

 

 7. 사용자로부터 입력 받기

  ⇒ 노드 내장 모듈 readline을 사용한다.

   - createInterface 메서드로 rl 객체를 선언하여 입출력을 설정한다.(process.stdin, process.stdout : 콘솔기본입력)

   - question 메서드로 질문을 표시하고 답변을 받아 콜백 함수를 실행한다.

   - 답변은 answer 매개변수에 담기며 마지막에 readline을 close처리해야 한다. 

#!/usr/bin/env node
const readline = require('readline');

// 터미널의 input과 output을 그대로 쓰겠다는 의미
const rl = readline.createInterface({
    input : process.stdin,
    output : process.stdout,
});

console.clear();

const answerCallback = (answer) => {
    if(answer.toUpperCase() === 'Y') {
        console.log('감사합니다.');
        rl.close();
    } else if(answer.toUpperCase() === 'N') {
        console.log('죄송합니다.');
        rl.close();
    } else {
        console.log('Y 또는 N만 입력하세요.');
        rl.question('예제가 재미있습니까? (y/n)', answerCallback);
    }
};
rl.question('예제가 재미있습니까? (y/n)', answerCallback);

 

$ npx cli

예제가 재미있습니까? (y/n)y
감사합니다.

 

 10. 템플릿을 만들어주는 명령어 만들기

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const readline = require('readline');
// npx cli html main .
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(error) {
        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.log('html 또는 express-router 둘 중 하나를 입력하세요.');
    }
};

const directoryAnswer = (answer) => {
    directory = answer?.trim() || '.';
    rl.close();
    makeTemplate();
}
const nameAnswer = (answer) => {
    if(answer === '') {
        console.clear();
        console.log('파일명은 반드시 입력해야 합니다.');
        return rl.question('어떤 파일명으로 하실건가요?', nameAnswer);
    } else {
        name = answer;
        return rl.question('파일의 디렉토리 경로를 입력하세요.', directoryAnswer);
    }
}
const typeAnswer = (answer) => {
    if(answer !== 'html' && answer !== 'express-router') {
        console.clear();
        console.log('html 또는 express-router 둘 중 하나를 입력하세요.');
        return rl.question('어떤 템플릿이 필요 하십니까?', typeAnswer);
    } else {
        type = answer;
        return rl.question('어떤 파일명으로 하실건가요?', nameAnswer);
    }
}
const program = () => {
    if(!type || !name) {
        console.error('사용방법 : cli html|express-router 파일명 [생성경로]');
        rl = readline.createInterface({
            input : process.stdin,
            output : process.stdout,
        });
        console.clear();
        rl.question('어떤 템플릿이 필요 하십니까?', typeAnswer);

    } else {
        makeTemplate();
    }
}

program();

14.2 Commander, Inquirer 사용하기

 1. 패키지로 쉽게 CLI 프로그램 만들기

  ⇒ npm에는 CLI 프로그램을 위한 라이브러리가 많이 준비되어 있다.

   - commander(CLI)와 inquirer(사용자와 상호작용), chalk(콘솔에 컬러)를 사용해서 예제생성

 2. commander 사용하기

  ⇒ command.js 파일 작성

   - version : 프로그램의 버전 설정(--version 또는 -v로 확인가능)

   - usage : 프로그램 사용 방법을 기입한다(--help 또는 -h로 확인)

   - command : 명령어를 등록한다.(template <type> 등)

   - description : 명령어에 대한 설명을 설정할 수 있다.

   - option : 명령어에 대한 옵션을 등록한다.(--옵션 [값] 또는 --옵션 <값> 형식, 두번째 인자는 설명, 세 번째 인자는 기본값)

   - Action : 명령어가 실행될 때 수행할 동작등록

   - parse : process.argv를 파싱하여 옵션을 등록한다.

 6. inquirer 사용하기

  ⇒ 옵션들을 외워 사용하는 것이 불편하기 때문에 상호작용을 위한 inquirer 패키지를 설치하여 사용한다.

    

#!/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 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(error) {
        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');

// <type> : 필수, [filename] : 선택
program
    .command('template <type>')
    .alias('tmpl')
    .usage('<type> --filename [filename] --path [path]')
    .description('템플릿을 생성합니다.')
    .option('-f --filename [filename]', '파일명을 입력하세요', 'index')
    .option('-d, --directory [path]', '생성 경로를 입력하세요', '.')
    .action((type, options, command) => {
        console.log(type, options.filename, options.directory);
        makeTemplate(type, options.filename, options.directory);
    });

program
    .action((options, command) => {
        if(command.args.length !== 0) {
            console.error(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.hex('#123fff')('터미널을 종료합니다.'));
                }
            });
        }

    })

program.parse(process.argv);