본문 바로가기
개발 기록

Node.js 로 CLI 프로그램 만들기 - 1. 강좌 따라가기

by 유세지 2020. 2. 21.

이 글은 zerocho님의 Node.js 강의 영상을 참고하여 진행하였습니다. 아래 예제들과 설명은 이 강의에 출처가 있음을 밝힙니다.(https://osam.kr/learn/lecture/15158/%EC%9E%90%EC%9C%A0%EA%B3%BC%EC%A0%95-web%EB%B6%84%EC%95%BC-node-js-%EA%B8%B0%EB%B3%B8%EB%B6%80%ED%84%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%A4%EC%8A%B5%EA%B9%8C%EC%A7%80-1/lesson/704252/72-cli-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EB%A7%8C%EB%93%A4%EA%B8%B0-cli-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-1)

 

goorm

구름은 클라우드 기술을 이용하여 누구나 코딩을 배우고, 실력을 평가하고, 소프트웨어를 개발할 수 있는 클라우드 소프트웨어 생태계입니다.

www.goorm.io

 

 

이번에 node.js를 이용해 웹 서비스를 만들며 노드에 대해 더 공부해보고 싶어졌다. 그래서 CLI (Command Line Interface) 프로그램을 한 번 만들어보려한다. 강의를 따라가며 구현에 필요한 명령어들을 알려주는 프로그램을 만들어보겠다. 

 

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);

 

readline의 기본적인 사용법이다.

터미널(프롬프트) 창에서 프로그램과 사용자가 입출력을 통한 상호작용 할 수 있도록 도와주는 패키지이다.

 

readline.createInterface({

    input: process.stdin,

    output: process.stdout,

});

 

위 코드를 통해 입력과 출력을 받으며,

readline.question('message', function); 을 통해 입력을 받고 그 값을 매개변수로 넘길 수 있다.

 

 

console.clear() 은 말 그대로 콘솔창을 깨끗하게 만들어준다.

 

 

 

다음은 이를 이용한 프로그램 예제입니다.

 

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 CLI!</h1>
</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 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 exist = () {
    try {
        fs.accessSync(dir, fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK);
        return true;
    } catch (e) {
        return false;
    }
};

const makeTemplate = () => {
	makdirp(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();
};

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();

 

처음보는 코드들이 있는건 아니라 이해가 어렵지는 않았다. html과 express-router 템플릿을 생성해주는 코드인데, CLI 프로그램의 기본적인 흐름을 볼 수 있는 것 같다.

 

 

 

 

하지만 코드가 너무 복잡하다. 이쁘게 보기 위해선 역시 프레임워크가 필요하다.

 

 

 

Commander 프레임워크 사용하기

 

Commander 는 CLI 프로그램을 만들때 유용한 npm 패키지이다.

npm install 명령어를 통해 설치하고 바로 사용해보겠다.

 

inquirer : 대화형 프로그램을 만드는 패키지

chalk : console에 색을 입혀주는 패키지

$ npm i commander inquirer chalk

 

yargs나 meow 같은 패키지도 있지만 우선은 commander만 해보자.

const program = require('commander');

program
    .version('0.0.1', '-v, --version') // 버젼
    .usage('[options]'); // 설명서(commander는 설명서를 자동생성해줌)
    
program
    .command('template <type>')
    .usage('--name <name> -- path [path]')
    .description('템플릿을 생성합니다.')
    .alias('tmpl')
    .option('-n, --name <name>', '파일명을 입력하세요', 'index')
    .option('-d, --directory [path]', '생성 경로를 입력하세요', '.') // 현재경로
    .action((type, options) => {
        console.log(type, options.name, options.directory);
    });
    
program
    .command('*', { noHelp: true }) // 도움말을 띄우지 말고,
    .action(() => {
        console.log('해당 명령어를 찾을 수 없습니다.');
        program.help();
    });
    
program.parse(process.argv);

if (!triggered) {
	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)('터미널을 종료합니다.'));
		}
	});
}
    
// * ===================================================    
// * 기호 설명
// * -- 옵션 - 단축옵션
// * <필수로 넣어야 하는 것> [선택적으로 넣어도 되는 것] 
// * *: 와일드카드 (나머지 처리)
// *
// * type: 프롬프트 종류
// * name: 질문명
// * message: 메시지
// * choices: 선택지
// * default: 기본값
// * ====================================================

 

 

위 코드처럼 commander를 이용하면 프로그램의 흐름을 파악하기 쉽고, 직관적으로 만들 수 있다. 한 번 익숙해지면 원래대로 돌아가기 어려워진다고 하던데 정말 맞는 말인 것 같다. commander의 몇 가지 속성들을 살펴보면,

 

.command : 실행 명령어

.version : 프로그램의 버젼

.usage : 사용법 ( 자동으로 생성됨 )

.description : 해당 명령어 설명

.alias : 별명. 이 프로그램을 실행하는 명령어의 약어

.option : 기능 명령어

.action : 실행 내용

 

이 정도가 있겠다.

 

아래 쪽 코드가 inquirer을 이용해 프로그램을 짠 것이다.

이 프로그램이 어떻게 진행되는지 한 눈에 들어와서 파악하기 좋다.

 

중간중간 콘솔로그에 있는 속성들은 chalk를 이용해 색을 입힌 것이다. green, red 처럼 색상을 입력해도 작동하고, rgb 코드를 입력해도 작동한다. 다만 해당 프롬프트에서 글자색을 지원해야 한다는 점.

 

이 외에 다른건 공식문서를 참고해보며 만들어보면 되겠다.

다음 글에서는 직접 프로그램을 만들어보며 정리해보겠다.

반응형

댓글